<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://candypedia.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Maintenance+script</id>
	<title>Candypedia - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://candypedia.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Maintenance+script"/>
	<link rel="alternate" type="text/html" href="https://candypedia.wiki/Special:Contributions/Maintenance_script"/>
	<updated>2026-06-10T10:22:08Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.42.0</generator>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3437</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3437"/>
		<updated>2026-01-06T01:16:49Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Change WikiEditor icons to articleCheck and code&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true (characters, locations)&lt;br /&gt;
            chapters: {},     // chapter title -&amp;gt; true (always link, case-sensitive)&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            // Chapters are stored separately - they&#039;re always linked (not just first occurrence)&lt;br /&gt;
                            // and matching is case-sensitive&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.chapters[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section.&lt;br /&gt;
                // Simplified regex to match &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; more robustly.&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    // Matches: &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; (case insensitive)&lt;br /&gt;
                    return /^(Major|Minor)?\s*Relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Helper: Check if an index in body is on a header line (line-based, not position-based)&lt;br /&gt;
        // This is more reliable than isInsideSectionHeader after body has been modified&lt;br /&gt;
        function isOnHeaderLine(text, idx) {&lt;br /&gt;
            // Find the line containing this index&lt;br /&gt;
            var lineStart = text.lastIndexOf(&#039;\n&#039;, idx - 1) + 1;&lt;br /&gt;
            var lineEnd = text.indexOf(&#039;\n&#039;, idx);&lt;br /&gt;
            if (lineEnd === -1) lineEnd = text.length;&lt;br /&gt;
            var line = text.substring(lineStart, lineEnd);&lt;br /&gt;
            // Check if this line is a section header (== ... ==)&lt;br /&gt;
            return /^={2,}[^=].*={2,}\s*$/.test(line);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Chapter Pass: Link ALL occurrences of chapter titles (case-sensitive, exact match)&lt;br /&gt;
        // Unlike characters which only get first occurrence linked, chapters are always linked.&lt;br /&gt;
        var chapterTitles = Object.keys(database.chapters).sort(function(a, b) {&lt;br /&gt;
            return b.length - a.length; // Longer titles first to avoid partial matches&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        chapterTitles.forEach(function(chapter) {&lt;br /&gt;
            // Case-sensitive, exact word boundary match&lt;br /&gt;
            var escapedChapter = chapter.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var chapterPattern = new RegExp(&#039;\\b(&#039; + escapedChapter + &#039;)\\b&#039;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var chapterMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((chapterMatch = chapterPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line&lt;br /&gt;
                if (isOnHeaderLine(body, chapterMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                var absPos = firstSectionIdx + chapterMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, chapterMatch.index - 50), chapterMatch.index);&lt;br /&gt;
                var after = body.substring(chapterMatch.index, Math.min(body.length, chapterMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Link this occurrence&lt;br /&gt;
                var captured = chapterMatch[1];&lt;br /&gt;
                var replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                &lt;br /&gt;
                newBody += body.substring(lastIndex, chapterMatch.index) + replacement;&lt;br /&gt;
                lastIndex = chapterMatch.index + chapterMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Linked chapter &amp;quot;&#039; + chapter + &#039;&amp;quot;&#039;);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (lastIndex &amp;gt; 0) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line (use line-based detection, not position-based)&lt;br /&gt;
                if (isOnHeaderLine(body, termMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section (position-based is OK here since wikitext hasn&#039;t changed)&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers, See also, or chapters&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            // Use line-based detection since body has been modified.&lt;br /&gt;
            if (isOnHeaderLine(body, dupMatch.index)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t deduplicate chapter links - chapters should ALWAYS be linked&lt;br /&gt;
            if (database.chapters[target]) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;articleCheck&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;code&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // VisualEditor integration removed - Parsoid causes too many issues with references and formatting&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;br /&gt;
// Force update timestamp: Mon Jan  5 19:24:00 EST 2026&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3436</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3436"/>
		<updated>2026-01-06T00:26:08Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Remove VisualEditor integration (Parsoid issues), change Fix All icon to check&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true (characters, locations)&lt;br /&gt;
            chapters: {},     // chapter title -&amp;gt; true (always link, case-sensitive)&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            // Chapters are stored separately - they&#039;re always linked (not just first occurrence)&lt;br /&gt;
                            // and matching is case-sensitive&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.chapters[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section.&lt;br /&gt;
                // Simplified regex to match &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; more robustly.&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    // Matches: &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; (case insensitive)&lt;br /&gt;
                    return /^(Major|Minor)?\s*Relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Helper: Check if an index in body is on a header line (line-based, not position-based)&lt;br /&gt;
        // This is more reliable than isInsideSectionHeader after body has been modified&lt;br /&gt;
        function isOnHeaderLine(text, idx) {&lt;br /&gt;
            // Find the line containing this index&lt;br /&gt;
            var lineStart = text.lastIndexOf(&#039;\n&#039;, idx - 1) + 1;&lt;br /&gt;
            var lineEnd = text.indexOf(&#039;\n&#039;, idx);&lt;br /&gt;
            if (lineEnd === -1) lineEnd = text.length;&lt;br /&gt;
            var line = text.substring(lineStart, lineEnd);&lt;br /&gt;
            // Check if this line is a section header (== ... ==)&lt;br /&gt;
            return /^={2,}[^=].*={2,}\s*$/.test(line);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Chapter Pass: Link ALL occurrences of chapter titles (case-sensitive, exact match)&lt;br /&gt;
        // Unlike characters which only get first occurrence linked, chapters are always linked.&lt;br /&gt;
        var chapterTitles = Object.keys(database.chapters).sort(function(a, b) {&lt;br /&gt;
            return b.length - a.length; // Longer titles first to avoid partial matches&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        chapterTitles.forEach(function(chapter) {&lt;br /&gt;
            // Case-sensitive, exact word boundary match&lt;br /&gt;
            var escapedChapter = chapter.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var chapterPattern = new RegExp(&#039;\\b(&#039; + escapedChapter + &#039;)\\b&#039;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var chapterMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((chapterMatch = chapterPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line&lt;br /&gt;
                if (isOnHeaderLine(body, chapterMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                var absPos = firstSectionIdx + chapterMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, chapterMatch.index - 50), chapterMatch.index);&lt;br /&gt;
                var after = body.substring(chapterMatch.index, Math.min(body.length, chapterMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Link this occurrence&lt;br /&gt;
                var captured = chapterMatch[1];&lt;br /&gt;
                var replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                &lt;br /&gt;
                newBody += body.substring(lastIndex, chapterMatch.index) + replacement;&lt;br /&gt;
                lastIndex = chapterMatch.index + chapterMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Linked chapter &amp;quot;&#039; + chapter + &#039;&amp;quot;&#039;);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (lastIndex &amp;gt; 0) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line (use line-based detection, not position-based)&lt;br /&gt;
                if (isOnHeaderLine(body, termMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section (position-based is OK here since wikitext hasn&#039;t changed)&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers, See also, or chapters&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            // Use line-based detection since body has been modified.&lt;br /&gt;
            if (isOnHeaderLine(body, dupMatch.index)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t deduplicate chapter links - chapters should ALWAYS be linked&lt;br /&gt;
            if (database.chapters[target]) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // VisualEditor integration removed - Parsoid causes too many issues with references and formatting&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;br /&gt;
// Force update timestamp: Mon Jan  5 19:24:00 EST 2026&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3435</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3435"/>
		<updated>2026-01-06T00:14:09Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Chapters always linked (case-sensitive), characters first occurrence only&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true (characters, locations)&lt;br /&gt;
            chapters: {},     // chapter title -&amp;gt; true (always link, case-sensitive)&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            // Chapters are stored separately - they&#039;re always linked (not just first occurrence)&lt;br /&gt;
                            // and matching is case-sensitive&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.chapters[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section.&lt;br /&gt;
                // Simplified regex to match &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; more robustly.&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    // Matches: &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; (case insensitive)&lt;br /&gt;
                    return /^(Major|Minor)?\s*Relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Helper: Check if an index in body is on a header line (line-based, not position-based)&lt;br /&gt;
        // This is more reliable than isInsideSectionHeader after body has been modified&lt;br /&gt;
        function isOnHeaderLine(text, idx) {&lt;br /&gt;
            // Find the line containing this index&lt;br /&gt;
            var lineStart = text.lastIndexOf(&#039;\n&#039;, idx - 1) + 1;&lt;br /&gt;
            var lineEnd = text.indexOf(&#039;\n&#039;, idx);&lt;br /&gt;
            if (lineEnd === -1) lineEnd = text.length;&lt;br /&gt;
            var line = text.substring(lineStart, lineEnd);&lt;br /&gt;
            // Check if this line is a section header (== ... ==)&lt;br /&gt;
            return /^={2,}[^=].*={2,}\s*$/.test(line);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Chapter Pass: Link ALL occurrences of chapter titles (case-sensitive, exact match)&lt;br /&gt;
        // Unlike characters which only get first occurrence linked, chapters are always linked.&lt;br /&gt;
        var chapterTitles = Object.keys(database.chapters).sort(function(a, b) {&lt;br /&gt;
            return b.length - a.length; // Longer titles first to avoid partial matches&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        chapterTitles.forEach(function(chapter) {&lt;br /&gt;
            // Case-sensitive, exact word boundary match&lt;br /&gt;
            var escapedChapter = chapter.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var chapterPattern = new RegExp(&#039;\\b(&#039; + escapedChapter + &#039;)\\b&#039;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var chapterMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((chapterMatch = chapterPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line&lt;br /&gt;
                if (isOnHeaderLine(body, chapterMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                var absPos = firstSectionIdx + chapterMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, chapterMatch.index - 50), chapterMatch.index);&lt;br /&gt;
                var after = body.substring(chapterMatch.index, Math.min(body.length, chapterMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Link this occurrence&lt;br /&gt;
                var captured = chapterMatch[1];&lt;br /&gt;
                var replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                &lt;br /&gt;
                newBody += body.substring(lastIndex, chapterMatch.index) + replacement;&lt;br /&gt;
                lastIndex = chapterMatch.index + chapterMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Linked chapter &amp;quot;&#039; + chapter + &#039;&amp;quot;&#039;);&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (lastIndex &amp;gt; 0) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line (use line-based detection, not position-based)&lt;br /&gt;
                if (isOnHeaderLine(body, termMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section (position-based is OK here since wikitext hasn&#039;t changed)&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers, See also, or chapters&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            // Use line-based detection since body has been modified.&lt;br /&gt;
            if (isOnHeaderLine(body, dupMatch.index)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t deduplicate chapter links - chapters should ALWAYS be linked&lt;br /&gt;
            if (database.chapters[target]) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;br /&gt;
// Force update timestamp: Mon Jan  5 18:52:11 EST 2026&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3434</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3434"/>
		<updated>2026-01-06T00:04:54Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix: Use line-based header detection instead of position-based (fixes header links being stripped)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section.&lt;br /&gt;
                // Simplified regex to match &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; more robustly.&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    // Matches: &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; (case insensitive)&lt;br /&gt;
                    return /^(Major|Minor)?\s*Relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Helper: Check if an index in body is on a header line (line-based, not position-based)&lt;br /&gt;
        // This is more reliable than isInsideSectionHeader after body has been modified&lt;br /&gt;
        function isOnHeaderLine(text, idx) {&lt;br /&gt;
            // Find the line containing this index&lt;br /&gt;
            var lineStart = text.lastIndexOf(&#039;\n&#039;, idx - 1) + 1;&lt;br /&gt;
            var lineEnd = text.indexOf(&#039;\n&#039;, idx);&lt;br /&gt;
            if (lineEnd === -1) lineEnd = text.length;&lt;br /&gt;
            var line = text.substring(lineStart, lineEnd);&lt;br /&gt;
            // Check if this line is a section header (== ... ==)&lt;br /&gt;
            return /^={2,}[^=].*={2,}\s*$/.test(line);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                // Skip if on a header line (use line-based detection, not position-based)&lt;br /&gt;
                if (isOnHeaderLine(body, termMatch.index)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section (position-based is OK here since wikitext hasn&#039;t changed)&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            // Use line-based detection since body has been modified.&lt;br /&gt;
            if (isOnHeaderLine(body, dupMatch.index)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;br /&gt;
// Force update timestamp: Mon Jan  5 18:52:11 EST 2026&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3433</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3433"/>
		<updated>2026-01-05T23:54:17Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Simplify Relationships regex and force update with debug logging&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section.&lt;br /&gt;
                // Simplified regex to match &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; more robustly.&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    // Matches: &amp;quot;Relationships&amp;quot;, &amp;quot;Major Relationships&amp;quot;, &amp;quot;Minor Relationships&amp;quot; (case insensitive)&lt;br /&gt;
                    return /^(Major|Minor)?\s*Relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                // LOGGING: Track decision path for specific headers (debug-20250105-2)&lt;br /&gt;
                if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                    console.log(&#039;DEBUG: Processing header &amp;quot;&#039; + title + &#039;&amp;quot; (Level &#039; + level + &#039;)&#039;);&lt;br /&gt;
                    console.log(&#039;DEBUG: Stack before push: &#039; + sectionStack.map(function(s) { return s.title + &#039;(&#039; + s.level + &#039;)&#039;; }).join(&#039; &amp;gt; &#039;));&lt;br /&gt;
                    console.log(&#039;DEBUG: isRelationships=&#039; + isRelationships);&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
                    &lt;br /&gt;
                    if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                        console.log(&#039;DEBUG: Lookup &amp;quot;&#039; + titleKey + &#039;&amp;quot; -&amp;gt; &#039; + canonical);&lt;br /&gt;
                    }&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                        &lt;br /&gt;
                        if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                            console.log(&#039;DEBUG: Changed to &#039; + line);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;br /&gt;
// Force update timestamp: Mon Jan  5 18:52:11 EST 2026&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3432</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3432"/>
		<updated>2026-01-05T23:52:17Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add debug logging for Relationships headers (timestamped)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                // LOGGING: Track decision path for specific headers (debug-20250105)&lt;br /&gt;
                if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                    console.log(&#039;DEBUG: Processing header &amp;quot;&#039; + title + &#039;&amp;quot;&#039;);&lt;br /&gt;
                    console.log(&#039;DEBUG: isRelationships=&#039; + isRelationships);&lt;br /&gt;
                    console.log(&#039;DEBUG: Stack context: &#039; + sectionStack.map(function(s) { return s.title; }).join(&#039; &amp;gt; &#039;));&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
                    &lt;br /&gt;
                    if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                        console.log(&#039;DEBUG: Lookup &amp;quot;&#039; + titleKey + &#039;&amp;quot; -&amp;gt; &#039; + canonical);&lt;br /&gt;
                    }&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                        &lt;br /&gt;
                        if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                            console.log(&#039;DEBUG: Changed to &#039; + line);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;br /&gt;
// Force update timestamp: Mon Jan  5 18:52:11 EST 2026&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3431</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3431"/>
		<updated>2026-01-05T23:49:43Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Debug Relationships header logic - Force Update&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;    /**&lt;br /&gt;
     * FormattingFixer for MediaWiki&lt;br /&gt;
     * &lt;br /&gt;
     * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
     * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
     * &lt;br /&gt;
     * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                // LOGGING: Track decision path for specific headers (debug-20250105)&lt;br /&gt;
                if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                    console.log(&#039;DEBUG: Processing header &amp;quot;&#039; + title + &#039;&amp;quot;&#039;);&lt;br /&gt;
                    console.log(&#039;DEBUG: isRelationships=&#039; + isRelationships);&lt;br /&gt;
                    console.log(&#039;DEBUG: Stack context: &#039; + sectionStack.map(function(s) { return s.title; }).join(&#039; &amp;gt; &#039;));&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
                    &lt;br /&gt;
                    if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                        console.log(&#039;DEBUG: Lookup &amp;quot;&#039; + titleKey + &#039;&amp;quot; -&amp;gt; &#039; + canonical);&lt;br /&gt;
                    }&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                        &lt;br /&gt;
                        if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                            console.log(&#039;DEBUG: Changed to &#039; + line);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3430</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3430"/>
		<updated>2026-01-05T23:47:44Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add debug logging with timestamp to force update&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                // LOGGING: Track decision path for specific headers (debug-20250105)&lt;br /&gt;
                if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                    console.log(&#039;DEBUG: Processing header &amp;quot;&#039; + title + &#039;&amp;quot;&#039;);&lt;br /&gt;
                    console.log(&#039;DEBUG: isRelationships=&#039; + isRelationships);&lt;br /&gt;
                    console.log(&#039;DEBUG: Stack context: &#039; + sectionStack.map(function(s) { return s.title; }).join(&#039; &amp;gt; &#039;));&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
                    &lt;br /&gt;
                    if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                        console.log(&#039;DEBUG: Lookup &amp;quot;&#039; + titleKey + &#039;&amp;quot; -&amp;gt; &#039; + canonical);&lt;br /&gt;
                    }&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                        &lt;br /&gt;
                        if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                            console.log(&#039;DEBUG: Changed to &#039; + line);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3429</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3429"/>
		<updated>2026-01-05T23:46:03Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Debug Relationships header logic&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                // LOGGING: Track decision path for specific headers&lt;br /&gt;
                if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                    console.log(&#039;DEBUG: Processing header &amp;quot;&#039; + title + &#039;&amp;quot;&#039;);&lt;br /&gt;
                    console.log(&#039;DEBUG: isRelationships=&#039; + isRelationships);&lt;br /&gt;
                    console.log(&#039;DEBUG: Stack context: &#039; + sectionStack.map(function(s) { return s.title; }).join(&#039; &amp;gt; &#039;));&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
                    &lt;br /&gt;
                    if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                        console.log(&#039;DEBUG: Lookup &amp;quot;&#039; + titleKey + &#039;&amp;quot; -&amp;gt; &#039; + canonical);&lt;br /&gt;
                    }&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                        &lt;br /&gt;
                        if (/^(Jessica|Daisy|Tess|Augustus)$/i.test(title)) {&lt;br /&gt;
                            console.log(&#039;DEBUG: Changed to &#039; + line);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3428</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3428"/>
		<updated>2026-01-05T23:41:05Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix Relationships header linkify: case-insensitive canonical lookup&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
&lt;br /&gt;
        // Case-insensitive lookup tables for header linkification.&lt;br /&gt;
        // Some pages may have headings like &amp;quot;=== jessica ===&amp;quot; or extra whitespace.&lt;br /&gt;
        // We normalize to lowercase and resolve to the canonical page title.&lt;br /&gt;
        var targetsByLower = {};&lt;br /&gt;
        for (var t in database.targets) {&lt;br /&gt;
            if (database.targets.hasOwnProperty(t)) {&lt;br /&gt;
                targetsByLower[t.toLowerCase()] = t;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        var aliasesByLower = {};&lt;br /&gt;
        for (var a in database.aliases) {&lt;br /&gt;
            if (database.aliases.hasOwnProperty(a)) {&lt;br /&gt;
                aliasesByLower[a.toLowerCase()] = database.aliases[a];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                    // Check if title is a known target/alias.&lt;br /&gt;
                    // Use case-insensitive lookup so &amp;quot;jessica&amp;quot; resolves to &amp;quot;Jessica&amp;quot;.&lt;br /&gt;
                    var titleKey = title.toLowerCase();&lt;br /&gt;
                    var canonical = targetsByLower[titleKey] || aliasesByLower[titleKey] || null;&lt;br /&gt;
&lt;br /&gt;
                    // Only link if known target/alias and not already linked.&lt;br /&gt;
                    // Use canonical title in the link to ensure proper capitalization.&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        line = headerMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + canonical + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3427</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3427"/>
		<updated>2026-01-05T23:31:05Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix self-linking on own page, fix single-bracket detection&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Get current page title to prevent self-linking&lt;br /&gt;
        var currentPageTitle = &#039;&#039;;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.config) {&lt;br /&gt;
            currentPageTitle = mw.config.get(&#039;wgTitle&#039;) || &#039;&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Mark current page as already linked to prevent self-linking&lt;br /&gt;
        if (currentPageTitle) {&lt;br /&gt;
            linked[currentPageTitle] = true;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link or inside single brackets (e.g., [Augustus] in quotes)&lt;br /&gt;
                // Check for [[ ]] (wikilinks) or [ ] (single brackets used in prose)&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                // Skip if inside wikilink [[ ... ]]&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                // Skip if inside single brackets [ ... ] (common in prose like &amp;quot;[Augustus] said&amp;quot;)&lt;br /&gt;
                if (/\[[^\[\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[\]]*\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3426</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3426"/>
		<updated>2026-01-05T23:22:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix: Header links no longer prevent first body occurrence from being linked&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link (simple heuristic: look for surrounding [[ ]])&lt;br /&gt;
                // This isn&#039;t perfect but handles simple cases where we might match inside a pipelink display text&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, and DON&#039;T mark them as seen.&lt;br /&gt;
            // Header links (e.g., === [[Augustus]] ===) are independent from body links.&lt;br /&gt;
            // The first body occurrence should still be linked even if a header link exists.&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen.&lt;br /&gt;
            // If a name appears in See also, we don&#039;t want to link it again in the body.&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3425</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3425"/>
		<updated>2026-01-05T23:16:23Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix auto-linking logic to ensure first instance is linked, update documentation&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Link Consistency: Ensures the FIRST occurrence of a term in the body (or Relationships header) is linked. &lt;br /&gt;
 *      If a later occurrence was manually linked but the first was not, it links the first and unlinks the later one.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        /* &lt;br /&gt;
         * PASS REMOVED: We no longer pre-mark existing links.&lt;br /&gt;
         * Instead, we let the Third Pass link the FIRST occurrence of a term.&lt;br /&gt;
         * The Fourth Pass (deduplication) will then clean up any subsequent links,&lt;br /&gt;
         * ensuring the first occurrence is always the one that is linked.&lt;br /&gt;
         */&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link (simple heuristic: look for surrounding [[ ]])&lt;br /&gt;
                // This isn&#039;t perfect but handles simple cases where we might match inside a pipelink display text&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers, but DO mark them as seen&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also, but DO mark them as seen&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3424</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3424"/>
		<updated>2026-01-05T23:04:27Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update documentation and comments&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * A comprehensive tool for standardizing citations and auto-linking content in wikitext.&lt;br /&gt;
 * Designed to work seamlessly with both VisualEditor (VE) and the classic WikiEditor.&lt;br /&gt;
 * &lt;br /&gt;
 * KEY FEATURES:&lt;br /&gt;
 * &lt;br /&gt;
 * 1. CITATION STANDARDIZATION&lt;br /&gt;
 *    - Reorders references: Ensures definitions (&amp;lt;ref name=&amp;quot;x&amp;quot;&amp;gt;...&amp;lt;/ref&amp;gt;) appear before reuses (&amp;lt;ref name=&amp;quot;x&amp;quot; /&amp;gt;).&lt;br /&gt;
 *    - Standardizes formats: Converts raw chapter URLs into {{Cite chapter|url=...}} templates.&lt;br /&gt;
 *    - Validates URLs: Checks that chapter URLs follow the /cXX/pXX pattern.&lt;br /&gt;
 *    - Renames References: Smartly renames generic ref names (e.g., &amp;quot;:0&amp;quot;) to chapter-based names (e.g., &amp;quot;c37p15&amp;quot;).&lt;br /&gt;
 *    - Fixes Undefined Refs: Identifies used but undefined references.&lt;br /&gt;
 * &lt;br /&gt;
 * 2. INTELLIGENT AUTO-LINKING&lt;br /&gt;
 *    - Database: Dynamically fetches link targets from Category:Characters, Category:Locations, and Template:ChapterList.&lt;br /&gt;
 *    - Alias Support: Respects manual aliases defined in Template:LinkifyAliases.&lt;br /&gt;
 *    - Smart Exclusions:&lt;br /&gt;
 *      - Skips the &amp;quot;Intro&amp;quot; section (text before the first section header) to preserve manual formatting and Infoboxes.&lt;br /&gt;
 *      - Skips the &amp;quot;See also&amp;quot; section to avoid modifying list items.&lt;br /&gt;
 *      - Ignores text already inside links ([[...]]) or section headers (== ... ==).&lt;br /&gt;
 *    - Relationships Logic: Specifically targets headers under &amp;quot;Relationships&amp;quot;, &amp;quot;Major relationships&amp;quot;, &lt;br /&gt;
 *      or &amp;quot;Minor relationships&amp;quot; sections (e.g., === Jessica ===) to auto-link character names, &lt;br /&gt;
 *      respecting the section hierarchy via a stack-based linear scan.&lt;br /&gt;
 * &lt;br /&gt;
 * 3. CONTENT PRESERVATION (VisualEditor)&lt;br /&gt;
 *    - Implements a &amp;quot;Split-Process-Merge&amp;quot; strategy for VisualEditor to prevent Parsoid corruption.&lt;br /&gt;
 *    - The Intro section (containing the Infobox and lead paragraph) is DETACHED before processing.&lt;br /&gt;
 *    - Only the body content is round-tripped through Parsoid for linking.&lt;br /&gt;
 *    - The original, untouched Intro is re-attached to the processed body at the end.&lt;br /&gt;
 *    - This guarantees that complex Infobox formatting (linebreaks) and lead text are preserved exactly.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link (simple heuristic: look for surrounding [[ ]])&lt;br /&gt;
                // This isn&#039;t perfect but handles simple cases where we might match inside a pipelink display text&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3422</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3422"/>
		<updated>2026-01-05T22:54:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update Relationships header logic to check full ancestor stack &amp;amp; add comments&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     * &lt;br /&gt;
     * Logic:&lt;br /&gt;
     * 1. Find the last &amp;quot;See also&amp;quot; header before the position.&lt;br /&gt;
     * 2. Check if there are any other headers between that &amp;quot;See also&amp;quot; and the position.&lt;br /&gt;
     * 3. If we find a header of the same level (e.g. == See also == ... == References ==) &lt;br /&gt;
     *    or higher level (e.g. === See also === ... == Next Section ==), we are no longer in &amp;quot;See also&amp;quot;.&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link (simple heuristic: look for surrounding [[ ]])&lt;br /&gt;
                // This isn&#039;t perfect but handles simple cases where we might match inside a pipelink display text&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     * &lt;br /&gt;
     * Strategy:&lt;br /&gt;
     * 1. Access the document&#039;s internal list of reference groups&lt;br /&gt;
     * 2. Iterate through all refs in the &amp;quot;mwReference&amp;quot; group&lt;br /&gt;
     * 3. extracting the raw wikitext content from the &amp;quot;mw&amp;quot; attribute or node content&lt;br /&gt;
     * 4. Apply fixes (like wrapping raw URLs in {{Cite chapter}})&lt;br /&gt;
     * 5. Create a transaction to update the &amp;quot;mw&amp;quot; attribute with the new content&lt;br /&gt;
     * 6. Apply all transactions to the surface&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3421</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3421"/>
		<updated>2026-01-05T22:52:39Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Improve Relationships header linking (nested support) and add code comments&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section hierarchy&lt;br /&gt;
                // Scan up the stack to find if any ancestor is a &amp;quot;Relationships&amp;quot; section&lt;br /&gt;
                var isRelationships = sectionStack.some(function(section) {&lt;br /&gt;
                    return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(section.title);&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link (simple heuristic: look for surrounding [[ ]])&lt;br /&gt;
                // This isn&#039;t perfect but handles simple cases where we might match inside a pipelink display text&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3420</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3420"/>
		<updated>2026-01-05T22:51:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add comments and finalize Relationships header linking logic&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section&lt;br /&gt;
                // The parent is the last item in the stack (if any)&lt;br /&gt;
                // e.g. &amp;quot;== Major relationships ==&amp;quot; (level 2) is parent of &amp;quot;=== Jessica ===&amp;quot; (level 3)&lt;br /&gt;
                var parent = sectionStack.length &amp;gt; 0 ? sectionStack[sectionStack.length - 1] : null;&lt;br /&gt;
                &lt;br /&gt;
                // We want to link sub-headers under &amp;quot;Relationships&amp;quot;, &amp;quot;Major relationships&amp;quot;, or &amp;quot;Minor relationships&amp;quot;&lt;br /&gt;
                // Case-insensitive match for flexibility&lt;br /&gt;
                var isRelationships = parent &amp;amp;&amp;amp; /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(parent.title);&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships &amp;amp;&amp;amp; level &amp;gt; parent.level) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        // This prevents us from linking &amp;quot;Rachel&amp;quot; if &amp;quot;[[Rachel]]&amp;quot; is already present later in the text&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        // We scan for each term individually to ensure we find the FIRST instance in the text&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            // Escape regex special chars and look for whole word match&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            // Allow &amp;quot;Rachel&#039;s&amp;quot; to match &amp;quot;Rachel&amp;quot;&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link (simple heuristic: look for surrounding [[ ]])&lt;br /&gt;
                // This isn&#039;t perfect but handles simple cases where we might match inside a pipelink display text&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    // Preserve display text if it differs from canonical (e.g. alias or possessive)&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3419</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3419"/>
		<updated>2026-01-05T22:49:51Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix Relationships header linking with linear scan, add comments&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process section headers linearly to handle Relationships linking&lt;br /&gt;
        // This avoids complex regex lookbacks and handles nesting correctly via a stack&lt;br /&gt;
        // Section stack tracks current section level and title to determine if we are inside a &amp;quot;Relationships&amp;quot; hierarchy&lt;br /&gt;
        var lines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var newLines = [];&lt;br /&gt;
        var sectionStack = []; // Array of { level: number, title: string }&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; lines.length; i++) {&lt;br /&gt;
            var line = lines[i];&lt;br /&gt;
            var headerMatch = /^(={2,})\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            &lt;br /&gt;
            if (headerMatch) {&lt;br /&gt;
                var level = headerMatch[1].length;&lt;br /&gt;
                var title = headerMatch[2].trim();&lt;br /&gt;
                &lt;br /&gt;
                // Update stack: pop anything &amp;gt;= current level (since we are starting a new section at &#039;level&#039;)&lt;br /&gt;
                // e.g. if stack is [2, 3] and we see a level 2 header, we pop 3 and 2.&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Check if we are inside a Relationships section&lt;br /&gt;
                // The parent is the last item in the stack (if any)&lt;br /&gt;
                // e.g. &amp;quot;== Major relationships ==&amp;quot; (level 2) is parent of &amp;quot;=== Jessica ===&amp;quot; (level 3)&lt;br /&gt;
                var parent = sectionStack.length &amp;gt; 0 ? sectionStack[sectionStack.length - 1] : null;&lt;br /&gt;
                &lt;br /&gt;
                // We want to link sub-headers under &amp;quot;Relationships&amp;quot;, &amp;quot;Major relationships&amp;quot;, or &amp;quot;Minor relationships&amp;quot;&lt;br /&gt;
                // Case-insensitive match for flexibility&lt;br /&gt;
                var isRelationships = parent &amp;amp;&amp;amp; /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(parent.title);&lt;br /&gt;
                &lt;br /&gt;
                if (isRelationships &amp;amp;&amp;amp; level &amp;gt; parent.level) {&lt;br /&gt;
                     // Check if title is a target or alias&lt;br /&gt;
                     var canonical = database.targets[title] ? title : (database.aliases[title] || null);&lt;br /&gt;
                     &lt;br /&gt;
                     // Only link if known target/alias and not already linked&lt;br /&gt;
                     if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                         line = headerMatch[1] + &#039; [[&#039; + title + &#039;]] &#039; + headerMatch[1];&lt;br /&gt;
                         fixes.push(&#039;Linked &amp;quot;&#039; + title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                     }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
            newLines.push(line);&lt;br /&gt;
        }&lt;br /&gt;
        body = newLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3418</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3418"/>
		<updated>2026-01-05T22:20:41Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix Relationships header linking bug, remove infobox restoration code&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process Relationships section headers - always link character names there&lt;br /&gt;
        // Collect all matches first, then process backwards to preserve indices&lt;br /&gt;
        var relationshipsSections = /^(===+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var relMatch;&lt;br /&gt;
        var headerMatches = [];&lt;br /&gt;
        while ((relMatch = relationshipsSections.exec(body)) !== null) {&lt;br /&gt;
            headerMatches.push({&lt;br /&gt;
                fullMatch: relMatch[0],&lt;br /&gt;
                equals: relMatch[1],&lt;br /&gt;
                title: relMatch[2].trim(),&lt;br /&gt;
                index: relMatch.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Process backwards to preserve indices when modifying&lt;br /&gt;
        for (var h = headerMatches.length - 1; h &amp;gt;= 0; h--) {&lt;br /&gt;
            var hdr = headerMatches[h];&lt;br /&gt;
            var headerPos = firstSectionIdx + hdr.index;&lt;br /&gt;
            var parents = getParentSections(wikitext, headerPos);&lt;br /&gt;
            &lt;br /&gt;
            // Check if parent is Relationships, Major relationships, or Minor relationships&lt;br /&gt;
            var inRelationships = parents.some(function(p) {&lt;br /&gt;
                return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(p);&lt;br /&gt;
            });&lt;br /&gt;
            &lt;br /&gt;
            if (inRelationships &amp;amp;&amp;amp; database.targets[hdr.title]) {&lt;br /&gt;
                // This header should be linked if it&#039;s a plain character name&lt;br /&gt;
                if (!/\[\[/.test(hdr.fullMatch)) {&lt;br /&gt;
                    var newHeader = hdr.equals + &#039; [[&#039; + hdr.title + &#039;]] &#039; + hdr.equals;&lt;br /&gt;
                    body = body.substring(0, hdr.index) + newHeader + body.substring(hdr.index + hdr.fullMatch.length);&lt;br /&gt;
                    fixes.push(&#039;Linked &amp;quot;&#039; + hdr.title + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3417</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3417"/>
		<updated>2026-01-05T22:02:19Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix infobox preservation by applying combined wikitext in VE source mode; fix Relationships header auto-linking&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process Relationships subsection headers - always link character names there&lt;br /&gt;
        // Do this line-by-line to avoid regex exec index issues when mutating the body string.&lt;br /&gt;
        var bodyLines = body.split(&#039;\n&#039;);&lt;br /&gt;
        var sectionStack = [];&lt;br /&gt;
        for (var li = 0; li &amp;lt; bodyLines.length; li++) {&lt;br /&gt;
            var line = bodyLines[li];&lt;br /&gt;
            var headingMatch = /^(==+)\s*([^=]+?)\s*\1\s*$/.exec(line);&lt;br /&gt;
            if (headingMatch) {&lt;br /&gt;
                var level = headingMatch[1].length;&lt;br /&gt;
                var title = headingMatch[2].trim();&lt;br /&gt;
                while (sectionStack.length &amp;gt; 0 &amp;amp;&amp;amp; sectionStack[sectionStack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                    sectionStack.pop();&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                var parentTitle = sectionStack.length &amp;gt; 0 ? sectionStack[sectionStack.length - 1].title : &#039;&#039;;&lt;br /&gt;
                var inRelationships = /^(Major\s+)?relationships$|^minor\s+relationships$|^relationships$/i.test(parentTitle);&lt;br /&gt;
                if (inRelationships &amp;amp;&amp;amp; level &amp;gt;= 3) {&lt;br /&gt;
                    var headerTitle = title;&lt;br /&gt;
                    var canonical = database.targets[headerTitle] ? headerTitle : (database.aliases[headerTitle] || null);&lt;br /&gt;
                    if (canonical &amp;amp;&amp;amp; !/\[\[/.test(line)) {&lt;br /&gt;
                        bodyLines[li] = headingMatch[1] + &#039; [[&#039; + canonical + &#039;]] &#039; + headingMatch[1];&lt;br /&gt;
                        fixes.push(&#039;Linked &amp;quot;&#039; + headerTitle + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                sectionStack.push({ level: level, title: title });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = bodyLines.join(&#039;\n&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore linebreaks to collapsed infoboxes.&lt;br /&gt;
     * Parsoid may collapse {{Infobox ...|param=val|param2=val2}} onto one line.&lt;br /&gt;
     * This function expands them back to one parameter per line.&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxLinebreaks(wikitext) {&lt;br /&gt;
        var lines = wikitext.split(&#039;\n&#039;);&lt;br /&gt;
        var result = [];&lt;br /&gt;
        &lt;br /&gt;
        for (var lineIdx = 0; lineIdx &amp;lt; lines.length; lineIdx++) {&lt;br /&gt;
            var line = lines[lineIdx];&lt;br /&gt;
            &lt;br /&gt;
            // Check if this line contains a collapsed infobox (starts with {{Infobox and has multiple | on same line)&lt;br /&gt;
            if (/^\{\{[Ii]nfobox[^}]*\|[^}]*\|/.test(line) &amp;amp;&amp;amp; !/\n/.test(line)) {&lt;br /&gt;
                // This looks like a collapsed infobox - expand it&lt;br /&gt;
                var expanded = expandInfobox(line);&lt;br /&gt;
                result.push(expanded);&lt;br /&gt;
            } else {&lt;br /&gt;
                result.push(line);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result.join(&#039;\n&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Expand a single-line infobox into multi-line format&lt;br /&gt;
     */&lt;br /&gt;
    function expandInfobox(line) {&lt;br /&gt;
        // Find the infobox template name&lt;br /&gt;
        var match = /^\{\{([^|]+)/.exec(line);&lt;br /&gt;
        if (!match) return line;&lt;br /&gt;
        &lt;br /&gt;
        var templateName = match[1];&lt;br /&gt;
        var rest = line.substring(match[0].length);&lt;br /&gt;
        &lt;br /&gt;
        // Parse parameters respecting nested braces/brackets&lt;br /&gt;
        var params = [];&lt;br /&gt;
        var depth = 0;&lt;br /&gt;
        var currentParam = &#039;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; rest.length; i++) {&lt;br /&gt;
            var char = rest[i];&lt;br /&gt;
            var nextChar = rest[i + 1] || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            if (char === &#039;{&#039; &amp;amp;&amp;amp; nextChar === &#039;{&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;}&#039; &amp;amp;&amp;amp; nextChar === &#039;}&#039;) {&lt;br /&gt;
                if (depth &amp;gt; 0) {&lt;br /&gt;
                    depth--;&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                } else {&lt;br /&gt;
                    // End of template&lt;br /&gt;
                    if (currentParam.trim()) {&lt;br /&gt;
                        params.push(currentParam.trim());&lt;br /&gt;
                    }&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            } else if (char === &#039;[&#039; &amp;amp;&amp;amp; nextChar === &#039;[&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;]&#039; &amp;amp;&amp;amp; nextChar === &#039;]&#039;) {&lt;br /&gt;
                depth--;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;|&#039; &amp;amp;&amp;amp; depth === 0) {&lt;br /&gt;
                if (currentParam.trim()) {&lt;br /&gt;
                    params.push(currentParam.trim());&lt;br /&gt;
                }&lt;br /&gt;
                currentParam = &#039;&#039;;&lt;br /&gt;
            } else {&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build expanded infobox&lt;br /&gt;
        var expanded = &#039;{{&#039; + templateName + &#039;\n&#039;;&lt;br /&gt;
        for (var p = 0; p &amp;lt; params.length; p++) {&lt;br /&gt;
            expanded += &#039;| &#039; + params[p] + &#039;\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        expanded += &#039;}}&#039;;&lt;br /&gt;
        &lt;br /&gt;
        return expanded;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
&lt;br /&gt;
                            // To preserve infobox/intro formatting, apply finalWikitext in source mode&lt;br /&gt;
                            veEnsureSourceMode().then( function () {&lt;br /&gt;
                                var sourceSurface = veGetSurface();&lt;br /&gt;
                                if ( !sourceSurface ) {&lt;br /&gt;
                                    showResultMessage( result );&lt;br /&gt;
                                    return;&lt;br /&gt;
                                }&lt;br /&gt;
                                veReplaceAllWikitextInSourceSurface( sourceSurface, finalWikitext );&lt;br /&gt;
                                result.fixes.push( &#039;Applied changes in source mode to preserve infobox formatting&#039; );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            }, function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract the intro section (between infobox/start and first == heading)&lt;br /&gt;
     */&lt;br /&gt;
    function extractIntroSection( wikitext ) {&lt;br /&gt;
        var firstHeading = /^==[^=]/m.exec( wikitext );&lt;br /&gt;
        if ( !firstHeading ) return { text: &#039;&#039;, start: 0, end: 0 };&lt;br /&gt;
        &lt;br /&gt;
        var introEnd = firstHeading.index;&lt;br /&gt;
        var introStart = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Skip past any infobox at the start&lt;br /&gt;
        var infoboxMatch = /^\{\{[Ii]nfobox[\s\S]*?\}\}\s*/m.exec( wikitext );&lt;br /&gt;
        if ( infoboxMatch &amp;amp;&amp;amp; infoboxMatch.index === 0 ) {&lt;br /&gt;
            introStart = infoboxMatch[0].length;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            text: wikitext.substring( introStart, introEnd ),&lt;br /&gt;
            start: introStart,&lt;br /&gt;
            end: introEnd&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract all infoboxes with their original formatting&lt;br /&gt;
     */&lt;br /&gt;
    function extractInfoboxes( wikitext ) {&lt;br /&gt;
        var infoboxes = [];&lt;br /&gt;
        var pattern = /\{\{[Ii]nfobox[\s\S]*?\}\}/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ( ( match = pattern.exec( wikitext ) ) !== null ) {&lt;br /&gt;
            infoboxes.push( {&lt;br /&gt;
                text: match[0],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
        return infoboxes;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore the intro section if Parsoid deleted it&lt;br /&gt;
     */&lt;br /&gt;
    function restoreIntroSection( wikitext, originalIntro, processedWikitext ) {&lt;br /&gt;
        if ( !originalIntro.text.trim() ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Get what the intro SHOULD be from the processed wikitext&lt;br /&gt;
        var processedIntro = extractIntroSection( processedWikitext );&lt;br /&gt;
        if ( !processedIntro.text.trim() ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Check if current wikitext has the intro&lt;br /&gt;
        var currentIntro = extractIntroSection( wikitext );&lt;br /&gt;
        &lt;br /&gt;
        // If the intro is missing or substantially shorter, restore it&lt;br /&gt;
        if ( currentIntro.text.trim().length &amp;lt; processedIntro.text.trim().length * 0.5 ) {&lt;br /&gt;
            // Find where to insert the intro (after infobox, before first heading)&lt;br /&gt;
            var insertPoint = currentIntro.start;&lt;br /&gt;
            var firstHeading = /^==[^=]/m.exec( wikitext );&lt;br /&gt;
            if ( firstHeading ) {&lt;br /&gt;
                // Insert the processed intro before the first heading&lt;br /&gt;
                return wikitext.substring( 0, firstHeading.index ) + &lt;br /&gt;
                       processedIntro.text + &lt;br /&gt;
                       wikitext.substring( firstHeading.index );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore original infobox structure if Parsoid collapsed parameters&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxStructure( wikitext, originalInfoboxes ) {&lt;br /&gt;
        if ( originalInfoboxes.length === 0 ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // For each infobox in current wikitext, try to restore original formatting&lt;br /&gt;
        var pattern = /\{\{[Ii]nfobox[^}]*\}\}/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        var offset = 0;&lt;br /&gt;
        &lt;br /&gt;
        while ( ( match = pattern.exec( wikitext ) ) !== null ) {&lt;br /&gt;
            var currentInfobox = match[0];&lt;br /&gt;
            &lt;br /&gt;
            // Find matching original infobox by type&lt;br /&gt;
            var typeMatch = /\{\{([Ii]nfobox[^|}\n]*)/i.exec( currentInfobox );&lt;br /&gt;
            if ( !typeMatch ) continue;&lt;br /&gt;
            var type = typeMatch[1].toLowerCase().trim();&lt;br /&gt;
            &lt;br /&gt;
            for ( var i = 0; i &amp;lt; originalInfoboxes.length; i++ ) {&lt;br /&gt;
                var origTypeMatch = /\{\{([Ii]nfobox[^|}\n]*)/i.exec( originalInfoboxes[i].text );&lt;br /&gt;
                if ( !origTypeMatch ) continue;&lt;br /&gt;
                var origType = origTypeMatch[1].toLowerCase().trim();&lt;br /&gt;
                &lt;br /&gt;
                if ( type === origType &amp;amp;&amp;amp; originalInfoboxes[i].text.indexOf( &#039;\n&#039; ) !== -1 ) {&lt;br /&gt;
                    // Original was multi-line, restore it&lt;br /&gt;
                    var pos = match.index + offset;&lt;br /&gt;
                    wikitext = wikitext.substring( 0, pos ) + &lt;br /&gt;
                               originalInfoboxes[i].text + &lt;br /&gt;
                               wikitext.substring( pos + currentInfobox.length );&lt;br /&gt;
                    offset += originalInfoboxes[i].text.length - currentInfobox.length;&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup,&lt;br /&gt;
        restoreInfoboxLinebreaks: restoreInfoboxLinebreaks&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3416</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3416"/>
		<updated>2026-01-05T21:47:11Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix intro preservation: split at first header, only Parsoid the body; toast 4/8s&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process Relationships section headers - always link character names there&lt;br /&gt;
        var relationshipsSections = /^(===+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var relMatch;&lt;br /&gt;
        var headerLinksAdded = {};&lt;br /&gt;
        while ((relMatch = relationshipsSections.exec(body)) !== null) {&lt;br /&gt;
            var headerTitle = relMatch[2].trim();&lt;br /&gt;
            var headerPos = firstSectionIdx + relMatch.index;&lt;br /&gt;
            var parents = getParentSections(wikitext, headerPos);&lt;br /&gt;
            &lt;br /&gt;
            // Check if parent is Relationships, Major relationships, or Minor relationships&lt;br /&gt;
            var inRelationships = parents.some(function(p) {&lt;br /&gt;
                return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(p);&lt;br /&gt;
            });&lt;br /&gt;
            &lt;br /&gt;
            if (inRelationships &amp;amp;&amp;amp; database.targets[headerTitle]) {&lt;br /&gt;
                // This header should be linked if it&#039;s a plain character name&lt;br /&gt;
                if (!/\[\[/.test(relMatch[0])) {&lt;br /&gt;
                    var newHeader = relMatch[1] + &#039; [[&#039; + headerTitle + &#039;]] &#039; + relMatch[1];&lt;br /&gt;
                    body = body.substring(0, relMatch.index) + newHeader + body.substring(relMatch.index + relMatch[0].length);&lt;br /&gt;
                    headerLinksAdded[headerTitle] = true;&lt;br /&gt;
                    fixes.push(&#039;Linked &amp;quot;&#039; + headerTitle + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    // Don&#039;t count this as &amp;quot;first link&amp;quot; for body text&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore linebreaks to collapsed infoboxes.&lt;br /&gt;
     * Parsoid may collapse {{Infobox ...|param=val|param2=val2}} onto one line.&lt;br /&gt;
     * This function expands them back to one parameter per line.&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxLinebreaks(wikitext) {&lt;br /&gt;
        var lines = wikitext.split(&#039;\n&#039;);&lt;br /&gt;
        var result = [];&lt;br /&gt;
        &lt;br /&gt;
        for (var lineIdx = 0; lineIdx &amp;lt; lines.length; lineIdx++) {&lt;br /&gt;
            var line = lines[lineIdx];&lt;br /&gt;
            &lt;br /&gt;
            // Check if this line contains a collapsed infobox (starts with {{Infobox and has multiple | on same line)&lt;br /&gt;
            if (/^\{\{[Ii]nfobox[^}]*\|[^}]*\|/.test(line) &amp;amp;&amp;amp; !/\n/.test(line)) {&lt;br /&gt;
                // This looks like a collapsed infobox - expand it&lt;br /&gt;
                var expanded = expandInfobox(line);&lt;br /&gt;
                result.push(expanded);&lt;br /&gt;
            } else {&lt;br /&gt;
                result.push(line);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result.join(&#039;\n&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Expand a single-line infobox into multi-line format&lt;br /&gt;
     */&lt;br /&gt;
    function expandInfobox(line) {&lt;br /&gt;
        // Find the infobox template name&lt;br /&gt;
        var match = /^\{\{([^|]+)/.exec(line);&lt;br /&gt;
        if (!match) return line;&lt;br /&gt;
        &lt;br /&gt;
        var templateName = match[1];&lt;br /&gt;
        var rest = line.substring(match[0].length);&lt;br /&gt;
        &lt;br /&gt;
        // Parse parameters respecting nested braces/brackets&lt;br /&gt;
        var params = [];&lt;br /&gt;
        var depth = 0;&lt;br /&gt;
        var currentParam = &#039;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; rest.length; i++) {&lt;br /&gt;
            var char = rest[i];&lt;br /&gt;
            var nextChar = rest[i + 1] || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            if (char === &#039;{&#039; &amp;amp;&amp;amp; nextChar === &#039;{&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;}&#039; &amp;amp;&amp;amp; nextChar === &#039;}&#039;) {&lt;br /&gt;
                if (depth &amp;gt; 0) {&lt;br /&gt;
                    depth--;&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                } else {&lt;br /&gt;
                    // End of template&lt;br /&gt;
                    if (currentParam.trim()) {&lt;br /&gt;
                        params.push(currentParam.trim());&lt;br /&gt;
                    }&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            } else if (char === &#039;[&#039; &amp;amp;&amp;amp; nextChar === &#039;[&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;]&#039; &amp;amp;&amp;amp; nextChar === &#039;]&#039;) {&lt;br /&gt;
                depth--;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;|&#039; &amp;amp;&amp;amp; depth === 0) {&lt;br /&gt;
                if (currentParam.trim()) {&lt;br /&gt;
                    params.push(currentParam.trim());&lt;br /&gt;
                }&lt;br /&gt;
                currentParam = &#039;&#039;;&lt;br /&gt;
            } else {&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build expanded infobox&lt;br /&gt;
        var expanded = &#039;{{&#039; + templateName + &#039;\n&#039;;&lt;br /&gt;
        for (var p = 0; p &amp;lt; params.length; p++) {&lt;br /&gt;
            expanded += &#039;| &#039; + params[p] + &#039;\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        expanded += &#039;}}&#039;;&lt;br /&gt;
        &lt;br /&gt;
        return expanded;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 4 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 8000 : 4000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, split intro from body to protect intro from Parsoid&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Split at first section header - intro stays untouched, only body goes through Parsoid&lt;br /&gt;
            var firstHeaderMatch = /^==[^=]/m.exec( originalWikitext );&lt;br /&gt;
            var introSection = &#039;&#039;;&lt;br /&gt;
            var bodySection = originalWikitext;&lt;br /&gt;
            &lt;br /&gt;
            if ( firstHeaderMatch ) {&lt;br /&gt;
                introSection = originalWikitext.substring( 0, firstHeaderMatch.index );&lt;br /&gt;
                bodySection = originalWikitext.substring( firstHeaderMatch.index );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Split the processed result the same way&lt;br /&gt;
                var processedFirstHeader = /^==[^=]/m.exec( result.wikitext );&lt;br /&gt;
                var processedBody = result.wikitext;&lt;br /&gt;
                if ( processedFirstHeader ) {&lt;br /&gt;
                    processedBody = result.wikitext.substring( processedFirstHeader.index );&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Only send the BODY through Parsoid, not the intro&lt;br /&gt;
                veCreateDmDocumentFromWikitext( processedBody, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid processes body, prepend the original intro unchanged&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidBody ) {&lt;br /&gt;
                            // Combine: original intro (unchanged) + Parsoid-processed body&lt;br /&gt;
                            var finalWikitext = introSection + parsoidBody;&lt;br /&gt;
                            &lt;br /&gt;
                            // Apply this combined result&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( finalWikitext, surface.getModel().getDocument() ).then( function ( finalDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, finalDoc );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } ).fail( function () {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract the intro section (between infobox/start and first == heading)&lt;br /&gt;
     */&lt;br /&gt;
    function extractIntroSection( wikitext ) {&lt;br /&gt;
        var firstHeading = /^==[^=]/m.exec( wikitext );&lt;br /&gt;
        if ( !firstHeading ) return { text: &#039;&#039;, start: 0, end: 0 };&lt;br /&gt;
        &lt;br /&gt;
        var introEnd = firstHeading.index;&lt;br /&gt;
        var introStart = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Skip past any infobox at the start&lt;br /&gt;
        var infoboxMatch = /^\{\{[Ii]nfobox[\s\S]*?\}\}\s*/m.exec( wikitext );&lt;br /&gt;
        if ( infoboxMatch &amp;amp;&amp;amp; infoboxMatch.index === 0 ) {&lt;br /&gt;
            introStart = infoboxMatch[0].length;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            text: wikitext.substring( introStart, introEnd ),&lt;br /&gt;
            start: introStart,&lt;br /&gt;
            end: introEnd&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract all infoboxes with their original formatting&lt;br /&gt;
     */&lt;br /&gt;
    function extractInfoboxes( wikitext ) {&lt;br /&gt;
        var infoboxes = [];&lt;br /&gt;
        var pattern = /\{\{[Ii]nfobox[\s\S]*?\}\}/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ( ( match = pattern.exec( wikitext ) ) !== null ) {&lt;br /&gt;
            infoboxes.push( {&lt;br /&gt;
                text: match[0],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
        return infoboxes;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore the intro section if Parsoid deleted it&lt;br /&gt;
     */&lt;br /&gt;
    function restoreIntroSection( wikitext, originalIntro, processedWikitext ) {&lt;br /&gt;
        if ( !originalIntro.text.trim() ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Get what the intro SHOULD be from the processed wikitext&lt;br /&gt;
        var processedIntro = extractIntroSection( processedWikitext );&lt;br /&gt;
        if ( !processedIntro.text.trim() ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Check if current wikitext has the intro&lt;br /&gt;
        var currentIntro = extractIntroSection( wikitext );&lt;br /&gt;
        &lt;br /&gt;
        // If the intro is missing or substantially shorter, restore it&lt;br /&gt;
        if ( currentIntro.text.trim().length &amp;lt; processedIntro.text.trim().length * 0.5 ) {&lt;br /&gt;
            // Find where to insert the intro (after infobox, before first heading)&lt;br /&gt;
            var insertPoint = currentIntro.start;&lt;br /&gt;
            var firstHeading = /^==[^=]/m.exec( wikitext );&lt;br /&gt;
            if ( firstHeading ) {&lt;br /&gt;
                // Insert the processed intro before the first heading&lt;br /&gt;
                return wikitext.substring( 0, firstHeading.index ) + &lt;br /&gt;
                       processedIntro.text + &lt;br /&gt;
                       wikitext.substring( firstHeading.index );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore original infobox structure if Parsoid collapsed parameters&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxStructure( wikitext, originalInfoboxes ) {&lt;br /&gt;
        if ( originalInfoboxes.length === 0 ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // For each infobox in current wikitext, try to restore original formatting&lt;br /&gt;
        var pattern = /\{\{[Ii]nfobox[^}]*\}\}/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        var offset = 0;&lt;br /&gt;
        &lt;br /&gt;
        while ( ( match = pattern.exec( wikitext ) ) !== null ) {&lt;br /&gt;
            var currentInfobox = match[0];&lt;br /&gt;
            &lt;br /&gt;
            // Find matching original infobox by type&lt;br /&gt;
            var typeMatch = /\{\{([Ii]nfobox[^|}\n]*)/i.exec( currentInfobox );&lt;br /&gt;
            if ( !typeMatch ) continue;&lt;br /&gt;
            var type = typeMatch[1].toLowerCase().trim();&lt;br /&gt;
            &lt;br /&gt;
            for ( var i = 0; i &amp;lt; originalInfoboxes.length; i++ ) {&lt;br /&gt;
                var origTypeMatch = /\{\{([Ii]nfobox[^|}\n]*)/i.exec( originalInfoboxes[i].text );&lt;br /&gt;
                if ( !origTypeMatch ) continue;&lt;br /&gt;
                var origType = origTypeMatch[1].toLowerCase().trim();&lt;br /&gt;
                &lt;br /&gt;
                if ( type === origType &amp;amp;&amp;amp; originalInfoboxes[i].text.indexOf( &#039;\n&#039; ) !== -1 ) {&lt;br /&gt;
                    // Original was multi-line, restore it&lt;br /&gt;
                    var pos = match.index + offset;&lt;br /&gt;
                    wikitext = wikitext.substring( 0, pos ) + &lt;br /&gt;
                               originalInfoboxes[i].text + &lt;br /&gt;
                               wikitext.substring( pos + currentInfobox.length );&lt;br /&gt;
                    offset += originalInfoboxes[i].text.length - currentInfobox.length;&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup,&lt;br /&gt;
        restoreInfoboxLinebreaks: restoreInfoboxLinebreaks&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3414</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3414"/>
		<updated>2026-01-05T21:35:52Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Bug fixes: intro preservation, infobox restoration, ref renaming, See also/header exceptions, Relationships auto-link, toast auto-hide&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Rename refs that have non-chapter-style names to proper chapter-based names.&lt;br /&gt;
     * E.g., name=&amp;quot;:0&amp;quot; with a chapter URL should become name=&amp;quot;c32p7&amp;quot;&lt;br /&gt;
     */&lt;br /&gt;
    function renameNonChapterStyleRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var renames = {}; // oldName -&amp;gt; newName mapping for updating reuses&lt;br /&gt;
        &lt;br /&gt;
        // Pattern for non-chapter-style names (like :0, :1, etc. or random strings)&lt;br /&gt;
        var nonChapterPattern = /^:[0-9]+$|^[^c]|^c[^0-9]/;&lt;br /&gt;
        &lt;br /&gt;
        // Process definitions that have non-chapter-style names but contain chapter URLs&lt;br /&gt;
        var defsToRename = refs.definitions.filter(function(def) {&lt;br /&gt;
            if (!nonChapterPattern.test(def.name)) return false;&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; }); // Process from end to preserve indices&lt;br /&gt;
        &lt;br /&gt;
        defsToRename.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already has proper name&lt;br /&gt;
            if (def.name === baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var newName = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(newName)) {&lt;br /&gt;
                newName = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(newName);&lt;br /&gt;
            renames[def.name] = newName;&lt;br /&gt;
            &lt;br /&gt;
            // Replace the ref definition with new name&lt;br /&gt;
            var newRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + newRef + wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Renamed ref &amp;quot;&#039; + def.name + &#039;&amp;quot; to &amp;quot;&#039; + newName + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Now update all reuses of renamed refs&lt;br /&gt;
        for (var oldName in renames) {&lt;br /&gt;
            var newName = renames[oldName];&lt;br /&gt;
            // Match both &amp;lt;ref name=&amp;quot;oldName&amp;quot; /&amp;gt; and &amp;lt;ref name=&amp;quot;oldName&amp;quot;/&amp;gt; (with or without space)&lt;br /&gt;
            var reusePattern = new RegExp(&#039;&amp;lt;ref\\s+name\\s*=\\s*[&amp;quot;\&#039;]&#039; + oldName.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;) + &#039;[&amp;quot;\&#039;]\\s*/&amp;gt;&#039;, &#039;gi&#039;);&lt;br /&gt;
            wikitext = wikitext.replace(reusePattern, &#039;&amp;lt;ref name=&amp;quot;&#039; + newName + &#039;&amp;quot; /&amp;gt;&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the index where the first section heading starts&lt;br /&gt;
     */&lt;br /&gt;
    function findFirstSectionIndex(wikitext) {&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        return sectionMatch ? sectionMatch.index : -1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if a position is inside a section header (== ... ==)&lt;br /&gt;
     */&lt;br /&gt;
    function isInsideSectionHeader(wikitext, position) {&lt;br /&gt;
        // Find the line containing this position&lt;br /&gt;
        var lineStart = wikitext.lastIndexOf(&#039;\n&#039;, position) + 1;&lt;br /&gt;
        var lineEnd = wikitext.indexOf(&#039;\n&#039;, position);&lt;br /&gt;
        if (lineEnd === -1) lineEnd = wikitext.length;&lt;br /&gt;
        var line = wikitext.substring(lineStart, lineEnd);&lt;br /&gt;
        return /^=+[^=]+=+\s*$/.test(line);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Check if position is within a &amp;quot;See also&amp;quot; section (until next same-level or higher heading)&lt;br /&gt;
     */&lt;br /&gt;
    function isInSeeAlsoSection(wikitext, position) {&lt;br /&gt;
        // Find all section headings before this position&lt;br /&gt;
        var beforeText = wikitext.substring(0, position);&lt;br /&gt;
        var seeAlsoPattern = /^(==+)\s*See\s+also\s*\1\s*$/gim;&lt;br /&gt;
        var lastSeeAlso = null;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = seeAlsoPattern.exec(beforeText)) !== null) {&lt;br /&gt;
            lastSeeAlso = { index: match.index, level: match[1].length };&lt;br /&gt;
        }&lt;br /&gt;
        if (!lastSeeAlso) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if there&#039;s a same-level or higher heading between See also and position&lt;br /&gt;
        var afterSeeAlso = wikitext.substring(lastSeeAlso.index);&lt;br /&gt;
        var nextHeadingPattern = /^(==+)\s*[^=]+\s*\1\s*$/gim;&lt;br /&gt;
        nextHeadingPattern.lastIndex = 0; // Skip the See also heading itself&lt;br /&gt;
        var firstMatch = true;&lt;br /&gt;
        while ((match = nextHeadingPattern.exec(afterSeeAlso)) !== null) {&lt;br /&gt;
            if (firstMatch) { firstMatch = false; continue; } // Skip See also itself&lt;br /&gt;
            var headingLevel = match[1].length;&lt;br /&gt;
            var absolutePos = lastSeeAlso.index + match.index;&lt;br /&gt;
            if (absolutePos &amp;lt; position &amp;amp;&amp;amp; headingLevel &amp;lt;= lastSeeAlso.level) {&lt;br /&gt;
                return false; // We&#039;ve left the See also section&lt;br /&gt;
            }&lt;br /&gt;
            if (absolutePos &amp;gt;= position) break;&lt;br /&gt;
        }&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the parent section context for a position (for Relationships detection)&lt;br /&gt;
     */&lt;br /&gt;
    function getParentSections(wikitext, position) {&lt;br /&gt;
        var sections = [];&lt;br /&gt;
        var headingPattern = /^(==+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var match;&lt;br /&gt;
        var stack = [];&lt;br /&gt;
        &lt;br /&gt;
        while ((match = headingPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            if (match.index &amp;gt;= position) break;&lt;br /&gt;
            var level = match[1].length;&lt;br /&gt;
            var title = match[2].trim();&lt;br /&gt;
            &lt;br /&gt;
            // Pop sections that are same level or higher&lt;br /&gt;
            while (stack.length &amp;gt; 0 &amp;amp;&amp;amp; stack[stack.length - 1].level &amp;gt;= level) {&lt;br /&gt;
                stack.pop();&lt;br /&gt;
            }&lt;br /&gt;
            stack.push({ level: level, title: title });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return stack.map(function(s) { return s.title; });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Find where the first section starts - everything before is intro (don&#039;t touch)&lt;br /&gt;
        var firstSectionIdx = findFirstSectionIndex(wikitext);&lt;br /&gt;
        if (firstSectionIdx === -1) {&lt;br /&gt;
            // No sections at all - don&#039;t linkify anything&lt;br /&gt;
            return { wikitext: wikitext, fixes: [] };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, firstSectionIdx);&lt;br /&gt;
        var body = wikitext.substring(firstSectionIdx);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) {&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First: Process Relationships section headers - always link character names there&lt;br /&gt;
        var relationshipsSections = /^(===+)\s*([^=]+?)\s*\1\s*$/gm;&lt;br /&gt;
        var relMatch;&lt;br /&gt;
        var headerLinksAdded = {};&lt;br /&gt;
        while ((relMatch = relationshipsSections.exec(body)) !== null) {&lt;br /&gt;
            var headerTitle = relMatch[2].trim();&lt;br /&gt;
            var headerPos = firstSectionIdx + relMatch.index;&lt;br /&gt;
            var parents = getParentSections(wikitext, headerPos);&lt;br /&gt;
            &lt;br /&gt;
            // Check if parent is Relationships, Major relationships, or Minor relationships&lt;br /&gt;
            var inRelationships = parents.some(function(p) {&lt;br /&gt;
                return /^(Major\s+)?[Rr]elationships$|^Minor\s+relationships$/i.test(p);&lt;br /&gt;
            });&lt;br /&gt;
            &lt;br /&gt;
            if (inRelationships &amp;amp;&amp;amp; database.targets[headerTitle]) {&lt;br /&gt;
                // This header should be linked if it&#039;s a plain character name&lt;br /&gt;
                if (!/\[\[/.test(relMatch[0])) {&lt;br /&gt;
                    var newHeader = relMatch[1] + &#039; [[&#039; + headerTitle + &#039;]] &#039; + relMatch[1];&lt;br /&gt;
                    body = body.substring(0, relMatch.index) + newHeader + body.substring(relMatch.index + relMatch[0].length);&lt;br /&gt;
                    headerLinksAdded[headerTitle] = true;&lt;br /&gt;
                    fixes.push(&#039;Linked &amp;quot;&#039; + headerTitle + &#039;&amp;quot; in Relationships header&#039;);&lt;br /&gt;
                    // Don&#039;t count this as &amp;quot;first link&amp;quot; for body text&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Find all existing links (not in headers) and mark as &amp;quot;linked&amp;quot;&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + match.index;&lt;br /&gt;
            // Skip links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
            // Skip links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
            &lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Add links for unlinked terms (first occurrence only, respecting exclusions)&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return;&lt;br /&gt;
            &lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var termPattern = new RegExp(&#039;\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            var newBody = &#039;&#039;;&lt;br /&gt;
            var lastIndex = 0;&lt;br /&gt;
            var termMatch;&lt;br /&gt;
            &lt;br /&gt;
            while ((termMatch = termPattern.exec(body)) !== null) {&lt;br /&gt;
                var absPos = firstSectionIdx + termMatch.index;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if inside a section header&lt;br /&gt;
                if (isInsideSectionHeader(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if in See also section&lt;br /&gt;
                if (isInSeeAlsoSection(wikitext, absPos)) continue;&lt;br /&gt;
                &lt;br /&gt;
                // Skip if already inside a link&lt;br /&gt;
                var before = body.substring(Math.max(0, termMatch.index - 50), termMatch.index);&lt;br /&gt;
                var after = body.substring(termMatch.index, Math.min(body.length, termMatch.index + 50));&lt;br /&gt;
                if (/\[\[[^\]]*$/.test(before) &amp;amp;&amp;amp; /^[^\[]*\]\]/.test(after)) continue;&lt;br /&gt;
                &lt;br /&gt;
                if (!found) {&lt;br /&gt;
                    found = true;&lt;br /&gt;
                    linked[item.canonical] = true;&lt;br /&gt;
                    &lt;br /&gt;
                    var captured = termMatch[1];&lt;br /&gt;
                    var replacement;&lt;br /&gt;
                    if (item.display || captured !== item.canonical) {&lt;br /&gt;
                        replacement = &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        replacement = &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    &lt;br /&gt;
                    newBody += body.substring(lastIndex, termMatch.index) + replacement;&lt;br /&gt;
                    lastIndex = termMatch.index + termMatch[0].length;&lt;br /&gt;
                    fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                body = newBody + body.substring(lastIndex);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Fourth pass: Remove duplicate links (keep first, remove subsequent) - but NOT in headers or See also&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        var dupPattern = /\[\[([^\]|]+)(\|([^\]]+))?\]\]/g;&lt;br /&gt;
        var newBody = &#039;&#039;;&lt;br /&gt;
        var lastIdx = 0;&lt;br /&gt;
        var dupMatch;&lt;br /&gt;
        &lt;br /&gt;
        while ((dupMatch = dupPattern.exec(body)) !== null) {&lt;br /&gt;
            var absPos = firstSectionIdx + dupMatch.index;&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in section headers&lt;br /&gt;
            if (isInsideSectionHeader(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t touch links in See also&lt;br /&gt;
            if (isInSeeAlsoSection(wikitext, absPos)) {&lt;br /&gt;
                continue;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            var target = dupMatch[1].trim();&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            &lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // Duplicate - remove link, keep text&lt;br /&gt;
                var text = dupMatch[3] || target;&lt;br /&gt;
                newBody += body.substring(lastIdx, dupMatch.index) + text;&lt;br /&gt;
                lastIdx = dupMatch.index + dupMatch[0].length;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                seenLinks[canonical] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        body = newBody + body.substring(lastIdx);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore linebreaks to collapsed infoboxes.&lt;br /&gt;
     * Parsoid may collapse {{Infobox ...|param=val|param2=val2}} onto one line.&lt;br /&gt;
     * This function expands them back to one parameter per line.&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxLinebreaks(wikitext) {&lt;br /&gt;
        var lines = wikitext.split(&#039;\n&#039;);&lt;br /&gt;
        var result = [];&lt;br /&gt;
        &lt;br /&gt;
        for (var lineIdx = 0; lineIdx &amp;lt; lines.length; lineIdx++) {&lt;br /&gt;
            var line = lines[lineIdx];&lt;br /&gt;
            &lt;br /&gt;
            // Check if this line contains a collapsed infobox (starts with {{Infobox and has multiple | on same line)&lt;br /&gt;
            if (/^\{\{[Ii]nfobox[^}]*\|[^}]*\|/.test(line) &amp;amp;&amp;amp; !/\n/.test(line)) {&lt;br /&gt;
                // This looks like a collapsed infobox - expand it&lt;br /&gt;
                var expanded = expandInfobox(line);&lt;br /&gt;
                result.push(expanded);&lt;br /&gt;
            } else {&lt;br /&gt;
                result.push(line);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result.join(&#039;\n&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Expand a single-line infobox into multi-line format&lt;br /&gt;
     */&lt;br /&gt;
    function expandInfobox(line) {&lt;br /&gt;
        // Find the infobox template name&lt;br /&gt;
        var match = /^\{\{([^|]+)/.exec(line);&lt;br /&gt;
        if (!match) return line;&lt;br /&gt;
        &lt;br /&gt;
        var templateName = match[1];&lt;br /&gt;
        var rest = line.substring(match[0].length);&lt;br /&gt;
        &lt;br /&gt;
        // Parse parameters respecting nested braces/brackets&lt;br /&gt;
        var params = [];&lt;br /&gt;
        var depth = 0;&lt;br /&gt;
        var currentParam = &#039;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; rest.length; i++) {&lt;br /&gt;
            var char = rest[i];&lt;br /&gt;
            var nextChar = rest[i + 1] || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            if (char === &#039;{&#039; &amp;amp;&amp;amp; nextChar === &#039;{&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;}&#039; &amp;amp;&amp;amp; nextChar === &#039;}&#039;) {&lt;br /&gt;
                if (depth &amp;gt; 0) {&lt;br /&gt;
                    depth--;&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                } else {&lt;br /&gt;
                    // End of template&lt;br /&gt;
                    if (currentParam.trim()) {&lt;br /&gt;
                        params.push(currentParam.trim());&lt;br /&gt;
                    }&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            } else if (char === &#039;[&#039; &amp;amp;&amp;amp; nextChar === &#039;[&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;]&#039; &amp;amp;&amp;amp; nextChar === &#039;]&#039;) {&lt;br /&gt;
                depth--;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;|&#039; &amp;amp;&amp;amp; depth === 0) {&lt;br /&gt;
                if (currentParam.trim()) {&lt;br /&gt;
                    params.push(currentParam.trim());&lt;br /&gt;
                }&lt;br /&gt;
                currentParam = &#039;&#039;;&lt;br /&gt;
            } else {&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build expanded infobox&lt;br /&gt;
        var expanded = &#039;{{&#039; + templateName + &#039;\n&#039;;&lt;br /&gt;
        for (var p = 0; p &amp;lt; params.length; p++) {&lt;br /&gt;
            expanded += &#039;| &#039; + params[p] + &#039;\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        expanded += &#039;}}&#039;;&lt;br /&gt;
        &lt;br /&gt;
        return expanded;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 5.5. Rename refs with non-chapter-style names (like :0) to proper chapter-based names&lt;br /&gt;
        var renameResult = renameNonChapterStyleRefs(wikitext, refs);&lt;br /&gt;
        if (renameResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = renameResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(renameResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        // Auto-hide after 8 seconds (longer if there are warnings)&lt;br /&gt;
        var hideDelay = result.warnings.length &amp;gt; 0 ? 12000 : 8000;&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                autoHideSeconds: hideDelay / 1000,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, use Parsoid but post-process to restore formatting&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( originalWikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Save original intro and infobox for restoration after Parsoid&lt;br /&gt;
            var originalIntro = extractIntroSection( originalWikitext );&lt;br /&gt;
            var originalInfoboxes = extractInfoboxes( originalWikitext );&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === originalWikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Parse new wikitext through Parsoid&lt;br /&gt;
                veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid, restore any mangled sections&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( parsoidWikitext ) {&lt;br /&gt;
                            var fixed = parsoidWikitext;&lt;br /&gt;
                            var restorations = [];&lt;br /&gt;
                            &lt;br /&gt;
                            // Restore intro section if it was deleted/changed&lt;br /&gt;
                            fixed = restoreIntroSection( fixed, originalIntro, result.wikitext );&lt;br /&gt;
                            if ( fixed !== parsoidWikitext ) {&lt;br /&gt;
                                restorations.push( &#039;Restored intro paragraph&#039; );&lt;br /&gt;
                            }&lt;br /&gt;
                            &lt;br /&gt;
                            // Restore infobox formatting&lt;br /&gt;
                            var afterIntro = fixed;&lt;br /&gt;
                            fixed = restoreInfoboxLinebreaks( fixed );&lt;br /&gt;
                            if ( fixed !== afterIntro ) {&lt;br /&gt;
                                restorations.push( &#039;Restored infobox formatting&#039; );&lt;br /&gt;
                            }&lt;br /&gt;
                            &lt;br /&gt;
                            // Restore original infobox structure if Parsoid mangled it&lt;br /&gt;
                            fixed = restoreInfoboxStructure( fixed, originalInfoboxes );&lt;br /&gt;
                            &lt;br /&gt;
                            if ( fixed !== parsoidWikitext ) {&lt;br /&gt;
                                veCreateDmDocumentFromWikitext( fixed, surface.getModel().getDocument() ).then( function ( fixedDoc ) {&lt;br /&gt;
                                    veReplaceAllContentWithDocument( surface, fixedDoc );&lt;br /&gt;
                                    result.fixes = result.fixes.concat( restorations );&lt;br /&gt;
                                    showResultMessage( result );&lt;br /&gt;
                                } ).fail( function () {&lt;br /&gt;
                                    // If restoration fails, still show original results&lt;br /&gt;
                                    showResultMessage( result );&lt;br /&gt;
                                } );&lt;br /&gt;
                            } else {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            }&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( originalWikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( originalWikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract the intro section (between infobox/start and first == heading)&lt;br /&gt;
     */&lt;br /&gt;
    function extractIntroSection( wikitext ) {&lt;br /&gt;
        var firstHeading = /^==[^=]/m.exec( wikitext );&lt;br /&gt;
        if ( !firstHeading ) return { text: &#039;&#039;, start: 0, end: 0 };&lt;br /&gt;
        &lt;br /&gt;
        var introEnd = firstHeading.index;&lt;br /&gt;
        var introStart = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Skip past any infobox at the start&lt;br /&gt;
        var infoboxMatch = /^\{\{[Ii]nfobox[\s\S]*?\}\}\s*/m.exec( wikitext );&lt;br /&gt;
        if ( infoboxMatch &amp;amp;&amp;amp; infoboxMatch.index === 0 ) {&lt;br /&gt;
            introStart = infoboxMatch[0].length;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            text: wikitext.substring( introStart, introEnd ),&lt;br /&gt;
            start: introStart,&lt;br /&gt;
            end: introEnd&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract all infoboxes with their original formatting&lt;br /&gt;
     */&lt;br /&gt;
    function extractInfoboxes( wikitext ) {&lt;br /&gt;
        var infoboxes = [];&lt;br /&gt;
        var pattern = /\{\{[Ii]nfobox[\s\S]*?\}\}/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ( ( match = pattern.exec( wikitext ) ) !== null ) {&lt;br /&gt;
            infoboxes.push( {&lt;br /&gt;
                text: match[0],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
        return infoboxes;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore the intro section if Parsoid deleted it&lt;br /&gt;
     */&lt;br /&gt;
    function restoreIntroSection( wikitext, originalIntro, processedWikitext ) {&lt;br /&gt;
        if ( !originalIntro.text.trim() ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Get what the intro SHOULD be from the processed wikitext&lt;br /&gt;
        var processedIntro = extractIntroSection( processedWikitext );&lt;br /&gt;
        if ( !processedIntro.text.trim() ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Check if current wikitext has the intro&lt;br /&gt;
        var currentIntro = extractIntroSection( wikitext );&lt;br /&gt;
        &lt;br /&gt;
        // If the intro is missing or substantially shorter, restore it&lt;br /&gt;
        if ( currentIntro.text.trim().length &amp;lt; processedIntro.text.trim().length * 0.5 ) {&lt;br /&gt;
            // Find where to insert the intro (after infobox, before first heading)&lt;br /&gt;
            var insertPoint = currentIntro.start;&lt;br /&gt;
            var firstHeading = /^==[^=]/m.exec( wikitext );&lt;br /&gt;
            if ( firstHeading ) {&lt;br /&gt;
                // Insert the processed intro before the first heading&lt;br /&gt;
                return wikitext.substring( 0, firstHeading.index ) + &lt;br /&gt;
                       processedIntro.text + &lt;br /&gt;
                       wikitext.substring( firstHeading.index );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore original infobox structure if Parsoid collapsed parameters&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxStructure( wikitext, originalInfoboxes ) {&lt;br /&gt;
        if ( originalInfoboxes.length === 0 ) return wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // For each infobox in current wikitext, try to restore original formatting&lt;br /&gt;
        var pattern = /\{\{[Ii]nfobox[^}]*\}\}/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        var offset = 0;&lt;br /&gt;
        &lt;br /&gt;
        while ( ( match = pattern.exec( wikitext ) ) !== null ) {&lt;br /&gt;
            var currentInfobox = match[0];&lt;br /&gt;
            &lt;br /&gt;
            // Find matching original infobox by type&lt;br /&gt;
            var typeMatch = /\{\{([Ii]nfobox[^|}\n]*)/i.exec( currentInfobox );&lt;br /&gt;
            if ( !typeMatch ) continue;&lt;br /&gt;
            var type = typeMatch[1].toLowerCase().trim();&lt;br /&gt;
            &lt;br /&gt;
            for ( var i = 0; i &amp;lt; originalInfoboxes.length; i++ ) {&lt;br /&gt;
                var origTypeMatch = /\{\{([Ii]nfobox[^|}\n]*)/i.exec( originalInfoboxes[i].text );&lt;br /&gt;
                if ( !origTypeMatch ) continue;&lt;br /&gt;
                var origType = origTypeMatch[1].toLowerCase().trim();&lt;br /&gt;
                &lt;br /&gt;
                if ( type === origType &amp;amp;&amp;amp; originalInfoboxes[i].text.indexOf( &#039;\n&#039; ) !== -1 ) {&lt;br /&gt;
                    // Original was multi-line, restore it&lt;br /&gt;
                    var pos = match.index + offset;&lt;br /&gt;
                    wikitext = wikitext.substring( 0, pos ) + &lt;br /&gt;
                               originalInfoboxes[i].text + &lt;br /&gt;
                               wikitext.substring( pos + currentInfobox.length );&lt;br /&gt;
                    offset += originalInfoboxes[i].text.length - currentInfobox.length;&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup,&lt;br /&gt;
        restoreInfoboxLinebreaks: restoreInfoboxLinebreaks&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=Template:LinkifyAliases&amp;diff=3336</id>
		<title>Template:LinkifyAliases</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=Template:LinkifyAliases&amp;diff=3336"/>
		<updated>2026-01-05T11:00:35Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Create linkify aliases template for FormattingFixer gadget&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- Template:LinkifyAliases --&amp;gt;&lt;br /&gt;
&amp;lt;!-- This template defines aliases for the FormattingFixer Auto Add Links feature --&amp;gt;&lt;br /&gt;
&amp;lt;!-- Format: alias1,alias2-&amp;gt;CanonicalPageName --&amp;gt;&lt;br /&gt;
&amp;lt;!-- Lines starting with &amp;lt;!-- are ignored --&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- Character Aliases --&amp;gt;&lt;br /&gt;
Mike&#039;s mom,Mike&#039;s mother-&amp;gt;Ellen&lt;br /&gt;
Lucy&#039;s dad-&amp;gt;Sam&lt;br /&gt;
Lucy&#039;s mom-&amp;gt;Jordan&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- Location Aliases with display text --&amp;gt;&lt;br /&gt;
&amp;lt;!-- Use these for when you want &amp;quot;aquarium&amp;quot; to link to [[Roseville Aquarium|aquarium]] --&amp;gt;&lt;br /&gt;
aquarium,the aquarium-&amp;gt;Roseville Aquarium&lt;br /&gt;
fishy place-&amp;gt;Roseville Aquarium&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- Add more aliases below --&amp;gt;&lt;br /&gt;
&amp;lt;!-- Each line should be: alias1,alias2,alias3-&amp;gt;CanonicalPageName --&amp;gt;&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3335</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3335"/>
		<updated>2026-01-05T11:00:20Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Auto Add Links: formatting cleanup, linkify from categories/templates, aliases support&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Auto Add Links - Formatting &amp;amp; Linkify&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Cache for linkify database (fetched from wiki)&lt;br /&gt;
     */&lt;br /&gt;
    var linkifyCache = null;&lt;br /&gt;
    var linkifyCacheTime = 0;&lt;br /&gt;
    var LINKIFY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply formatting cleanup to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyFormattingCleanup(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var original = wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Trim whitespace from start and end&lt;br /&gt;
        wikitext = wikitext.trim();&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Delete trailing spaces from lines&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/\s+$/, &#039;&#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3: Replace multiple spaces with single space (within lines)&lt;br /&gt;
        wikitext = wikitext.split(&#039;\n&#039;).map(function(line) {&lt;br /&gt;
            return line.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
        }).join(&#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Step 3.5: Replace ** markdown bold with &#039;&#039;&#039; wikitext bold&lt;br /&gt;
        if (/\*\*/.test(wikitext)) {&lt;br /&gt;
            wikitext = wikitext.replace(/\*\*/g, &amp;quot;&#039;&#039;&#039;&amp;quot;);&lt;br /&gt;
            fixes.push(&#039;Converted markdown bold to wikitext bold&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure single space after &amp;lt;/ref&amp;gt; if not at end of line&lt;br /&gt;
        wikitext = wikitext.replace(/&amp;lt;\/ref&amp;gt;(?![\s\n]|$)/g, &#039;&amp;lt;/ref&amp;gt; &#039;);&lt;br /&gt;
&lt;br /&gt;
        // Remove any double spaces that might have been created&lt;br /&gt;
        wikitext = wikitext.replace(/ {2,}/g, &#039; &#039;);&lt;br /&gt;
&lt;br /&gt;
        if (wikitext !== original) {&lt;br /&gt;
            fixes.push(&#039;Applied formatting cleanup&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fetch the linkify database from wiki categories and templates&lt;br /&gt;
     */&lt;br /&gt;
    function fetchLinkifyDatabase() {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
&lt;br /&gt;
        // Check cache&lt;br /&gt;
        if (linkifyCache &amp;amp;&amp;amp; (Date.now() - linkifyCacheTime &amp;lt; LINKIFY_CACHE_TTL)) {&lt;br /&gt;
            return deferred.resolve(linkifyCache).promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var database = {&lt;br /&gt;
            targets: {},      // canonical name -&amp;gt; true&lt;br /&gt;
            aliases: {},      // alias -&amp;gt; canonical name&lt;br /&gt;
            displayText: {}   // search term -&amp;gt; { target: canonical, display: text }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var apiCalls = [];&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Characters&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Characters&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Category:Locations&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                list: &#039;categorymembers&#039;,&lt;br /&gt;
                cmtitle: &#039;Category:Locations&#039;,&lt;br /&gt;
                cmlimit: 500,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                if (data.query &amp;amp;&amp;amp; data.query.categorymembers) {&lt;br /&gt;
                    data.query.categorymembers.forEach(function(member) {&lt;br /&gt;
                        database.targets[member.title] = true;&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:ChapterList content&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:ChapterList&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse {{Chapter|number=X|title=Y|...}} entries&lt;br /&gt;
                            var chapterPattern = /\{\{Chapter\|[^}]*title=([^|}]+)/gi;&lt;br /&gt;
                            var match;&lt;br /&gt;
                            while ((match = chapterPattern.exec(content)) !== null) {&lt;br /&gt;
                                var title = match[1].trim();&lt;br /&gt;
                                database.targets[title] = true;&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        // Fetch Template:LinkifyAliases for custom mappings&lt;br /&gt;
        apiCalls.push(&lt;br /&gt;
            new mw.Api().get({&lt;br /&gt;
                action: &#039;query&#039;,&lt;br /&gt;
                titles: &#039;Template:LinkifyAliases&#039;,&lt;br /&gt;
                prop: &#039;revisions&#039;,&lt;br /&gt;
                rvprop: &#039;content&#039;,&lt;br /&gt;
                rvslots: &#039;main&#039;,&lt;br /&gt;
                format: &#039;json&#039;&lt;br /&gt;
            }).then(function(data) {&lt;br /&gt;
                var pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
                if (pages) {&lt;br /&gt;
                    for (var pageId in pages) {&lt;br /&gt;
                        var page = pages[pageId];&lt;br /&gt;
                        if (page.revisions &amp;amp;&amp;amp; page.revisions[0]) {&lt;br /&gt;
                            var content = page.revisions[0].slots.main[&#039;*&#039;];&lt;br /&gt;
                            // Parse alias definitions&lt;br /&gt;
                            // Format: alias1,alias2-&amp;gt;CanonicalName&lt;br /&gt;
                            // Or: displayText-&amp;gt;CanonicalName (for piped links)&lt;br /&gt;
                            var lines = content.split(&#039;\n&#039;);&lt;br /&gt;
                            lines.forEach(function(line) {&lt;br /&gt;
                                line = line.trim();&lt;br /&gt;
                                if (!line || line.startsWith(&#039;&amp;lt;!--&#039;) || line.startsWith(&#039;{{&#039;) || line.startsWith(&#039;}}&#039;)) return;&lt;br /&gt;
                                &lt;br /&gt;
                                var arrowMatch = line.match(/^([^-]+)-&amp;gt;(.+)$/);&lt;br /&gt;
                                if (arrowMatch) {&lt;br /&gt;
                                    var aliases = arrowMatch[1].split(&#039;,&#039;).map(function(s) { return s.trim(); });&lt;br /&gt;
                                    var target = arrowMatch[2].trim();&lt;br /&gt;
                                    aliases.forEach(function(alias) {&lt;br /&gt;
                                        database.aliases[alias] = target;&lt;br /&gt;
                                        database.aliases[alias.toLowerCase()] = target;&lt;br /&gt;
                                    });&lt;br /&gt;
                                }&lt;br /&gt;
                            });&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }).fail(function() {&lt;br /&gt;
                // Template doesn&#039;t exist yet, that&#039;s fine&lt;br /&gt;
            })&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        $.when.apply($, apiCalls).always(function() {&lt;br /&gt;
            linkifyCache = database;&lt;br /&gt;
            linkifyCacheTime = Date.now();&lt;br /&gt;
            deferred.resolve(database);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find the start of the &amp;quot;body&amp;quot; content (after intro/infobox)&lt;br /&gt;
     */&lt;br /&gt;
    function findBodyStart(wikitext) {&lt;br /&gt;
        // Look for first section heading&lt;br /&gt;
        var sectionMatch = /^==\s*[^=]+\s*==/m.exec(wikitext);&lt;br /&gt;
        if (sectionMatch) {&lt;br /&gt;
            return sectionMatch.index;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // No sections - look for end of first infobox&lt;br /&gt;
        var infoboxMatch = /\{\{[Ii]nfobox[\s\S]*?\}\}/m.exec(wikitext);&lt;br /&gt;
        if (infoboxMatch) {&lt;br /&gt;
            return infoboxMatch.index + infoboxMatch[0].length;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // No infobox either - just skip the first paragraph&lt;br /&gt;
        var firstParaEnd = wikitext.indexOf(&#039;\n\n&#039;);&lt;br /&gt;
        if (firstParaEnd &amp;gt; 0) {&lt;br /&gt;
            return firstParaEnd;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return 0;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Apply linkify logic to wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function applyLinkify(wikitext, database) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var bodyStart = findBodyStart(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        var intro = wikitext.substring(0, bodyStart);&lt;br /&gt;
        var body = wikitext.substring(bodyStart);&lt;br /&gt;
        &lt;br /&gt;
        // Track which canonical targets have been linked&lt;br /&gt;
        var linked = {};&lt;br /&gt;
        &lt;br /&gt;
        // Build a combined list of all searchable terms&lt;br /&gt;
        var allTerms = [];&lt;br /&gt;
        &lt;br /&gt;
        // Add canonical targets&lt;br /&gt;
        for (var target in database.targets) {&lt;br /&gt;
            allTerms.push({ term: target, canonical: target, display: null });&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Add aliases&lt;br /&gt;
        for (var alias in database.aliases) {&lt;br /&gt;
            if (!database.targets[alias]) { // Don&#039;t duplicate&lt;br /&gt;
                allTerms.push({ term: alias, canonical: database.aliases[alias], display: alias });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Sort by length (longest first) to match longer phrases before shorter ones&lt;br /&gt;
        allTerms.sort(function(a, b) { return b.term.length - a.term.length; });&lt;br /&gt;
        &lt;br /&gt;
        // First pass: Find all existing links and mark their targets as &amp;quot;linked&amp;quot;&lt;br /&gt;
        var existingLinkPattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = existingLinkPattern.exec(body)) !== null) {&lt;br /&gt;
            var linkTarget = match[1].trim();&lt;br /&gt;
            // Check if this is a known target or alias&lt;br /&gt;
            if (database.targets[linkTarget]) {&lt;br /&gt;
                linked[linkTarget] = true;&lt;br /&gt;
            } else if (database.aliases[linkTarget]) {&lt;br /&gt;
                linked[database.aliases[linkTarget]] = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Second pass: Add links for unlinked terms (first occurrence only)&lt;br /&gt;
        allTerms.forEach(function(item) {&lt;br /&gt;
            if (linked[item.canonical]) return; // Already linked&lt;br /&gt;
            &lt;br /&gt;
            // Build regex to find the term (word boundaries, case-sensitive for proper names)&lt;br /&gt;
            var escapedTerm = item.term.replace(/[.*+?^${}()|[\]\\]/g, &#039;\\$&amp;amp;&#039;);&lt;br /&gt;
            var termPattern = new RegExp(&#039;(?&amp;lt;!\\[\\[)\\b(&#039; + escapedTerm + &amp;quot;(?:&#039;s)?)\\b(?![^\\[]*\\]\\])&amp;quot;, &#039;g&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var found = false;&lt;br /&gt;
            body = body.replace(termPattern, function(match, captured, offset) {&lt;br /&gt;
                if (found) return match; // Only link first occurrence&lt;br /&gt;
                found = true;&lt;br /&gt;
                linked[item.canonical] = true;&lt;br /&gt;
                &lt;br /&gt;
                if (item.display || captured !== item.canonical) {&lt;br /&gt;
                    // Need piped link&lt;br /&gt;
                    return &#039;[[&#039; + item.canonical + &#039;|&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    return &#039;[[&#039; + captured + &#039;]]&#039;;&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            &lt;br /&gt;
            if (found) {&lt;br /&gt;
                fixes.push(&#039;Linked first instance of &amp;quot;&#039; + item.term + &#039;&amp;quot;&#039;);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        // Third pass: Remove duplicate links (keep first, remove subsequent)&lt;br /&gt;
        var seenLinks = {};&lt;br /&gt;
        body = body.replace(/\[\[([^\]|]+)(\|[^\]]+)?\]\]/g, function(match, target, display) {&lt;br /&gt;
            var canonical = database.aliases[target] || target;&lt;br /&gt;
            if (seenLinks[canonical]) {&lt;br /&gt;
                // This is a duplicate - remove the link, keep the text&lt;br /&gt;
                var text = display ? display.substring(1) : target;&lt;br /&gt;
                fixes.push(&#039;Removed duplicate link to &amp;quot;&#039; + target + &#039;&amp;quot;&#039;);&lt;br /&gt;
                return text;&lt;br /&gt;
            }&lt;br /&gt;
            seenLinks[canonical] = true;&lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: intro + body,&lt;br /&gt;
            fixes: fixes&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links - main function&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Step 1: Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Step 2: Fetch linkify database and apply&lt;br /&gt;
        fetchLinkifyDatabase().then(function(database) {&lt;br /&gt;
            var targetCount = Object.keys(database.targets).length;&lt;br /&gt;
            var aliasCount = Object.keys(database.aliases).length;&lt;br /&gt;
            &lt;br /&gt;
            if (targetCount === 0) {&lt;br /&gt;
                warnings.push(&#039;No linkify targets found. Create Category:Characters, Category:Locations, or Template:ChapterList.&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                var linkResult = applyLinkify(wikitext, database);&lt;br /&gt;
                wikitext = linkResult.wikitext;&lt;br /&gt;
                allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        }).fail(function() {&lt;br /&gt;
            warnings.push(&#039;Could not fetch linkify database. Formatting cleanup was still applied.&#039;);&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: wikitext,&lt;br /&gt;
                fixes: allFixes,&lt;br /&gt;
                warnings: warnings&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of autoAddLinks for source mode (uses cached database)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinksSync(wikitext) {&lt;br /&gt;
        var allFixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
&lt;br /&gt;
        // Apply formatting cleanup&lt;br /&gt;
        var formatResult = applyFormattingCleanup(wikitext);&lt;br /&gt;
        wikitext = formatResult.wikitext;&lt;br /&gt;
        allFixes = allFixes.concat(formatResult.fixes);&lt;br /&gt;
&lt;br /&gt;
        // Use cached database if available&lt;br /&gt;
        if (linkifyCache) {&lt;br /&gt;
            var linkResult = applyLinkify(wikitext, linkifyCache);&lt;br /&gt;
            wikitext = linkResult.wikitext;&lt;br /&gt;
            allFixes = allFixes.concat(linkResult.fixes);&lt;br /&gt;
        } else {&lt;br /&gt;
            warnings.push(&#039;Linkify database not cached. Run Auto Add Links in Visual Editor first, or wait a moment.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: allFixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links (async)&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        var deferred = $.Deferred();&lt;br /&gt;
        &lt;br /&gt;
        // Run Fix Citations first (sync)&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result (async)&lt;br /&gt;
        autoAddLinks(citationResult.wikitext).then(function(linkResult) {&lt;br /&gt;
            deferred.resolve({&lt;br /&gt;
                wikitext: linkResult.wikitext,&lt;br /&gt;
                fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
                warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return deferred.promise();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Synchronous version of fixAll for source mode&lt;br /&gt;
     */&lt;br /&gt;
    function fixAllSync(wikitext) {&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        var linkResult = autoAddLinksSync(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore linebreaks to collapsed infoboxes.&lt;br /&gt;
     * Parsoid may collapse {{Infobox ...|param=val|param2=val2}} onto one line.&lt;br /&gt;
     * This function expands them back to one parameter per line.&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxLinebreaks(wikitext) {&lt;br /&gt;
        var lines = wikitext.split(&#039;\n&#039;);&lt;br /&gt;
        var result = [];&lt;br /&gt;
        &lt;br /&gt;
        for (var lineIdx = 0; lineIdx &amp;lt; lines.length; lineIdx++) {&lt;br /&gt;
            var line = lines[lineIdx];&lt;br /&gt;
            &lt;br /&gt;
            // Check if this line contains a collapsed infobox (starts with {{Infobox and has multiple | on same line)&lt;br /&gt;
            if (/^\{\{[Ii]nfobox[^}]*\|[^}]*\|/.test(line) &amp;amp;&amp;amp; !/\n/.test(line)) {&lt;br /&gt;
                // This looks like a collapsed infobox - expand it&lt;br /&gt;
                var expanded = expandInfobox(line);&lt;br /&gt;
                result.push(expanded);&lt;br /&gt;
            } else {&lt;br /&gt;
                result.push(line);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return result.join(&#039;\n&#039;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Expand a single-line infobox into multi-line format&lt;br /&gt;
     */&lt;br /&gt;
    function expandInfobox(line) {&lt;br /&gt;
        // Find the infobox template name&lt;br /&gt;
        var match = /^\{\{([^|]+)/.exec(line);&lt;br /&gt;
        if (!match) return line;&lt;br /&gt;
        &lt;br /&gt;
        var templateName = match[1];&lt;br /&gt;
        var rest = line.substring(match[0].length);&lt;br /&gt;
        &lt;br /&gt;
        // Parse parameters respecting nested braces/brackets&lt;br /&gt;
        var params = [];&lt;br /&gt;
        var depth = 0;&lt;br /&gt;
        var currentParam = &#039;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        for (var i = 0; i &amp;lt; rest.length; i++) {&lt;br /&gt;
            var char = rest[i];&lt;br /&gt;
            var nextChar = rest[i + 1] || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            if (char === &#039;{&#039; &amp;amp;&amp;amp; nextChar === &#039;{&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;}&#039; &amp;amp;&amp;amp; nextChar === &#039;}&#039;) {&lt;br /&gt;
                if (depth &amp;gt; 0) {&lt;br /&gt;
                    depth--;&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                } else {&lt;br /&gt;
                    // End of template&lt;br /&gt;
                    if (currentParam.trim()) {&lt;br /&gt;
                        params.push(currentParam.trim());&lt;br /&gt;
                    }&lt;br /&gt;
                    break;&lt;br /&gt;
                }&lt;br /&gt;
            } else if (char === &#039;[&#039; &amp;amp;&amp;amp; nextChar === &#039;[&#039;) {&lt;br /&gt;
                depth++;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;]&#039; &amp;amp;&amp;amp; nextChar === &#039;]&#039;) {&lt;br /&gt;
                depth--;&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            } else if (char === &#039;|&#039; &amp;amp;&amp;amp; depth === 0) {&lt;br /&gt;
                if (currentParam.trim()) {&lt;br /&gt;
                    params.push(currentParam.trim());&lt;br /&gt;
                }&lt;br /&gt;
                currentParam = &#039;&#039;;&lt;br /&gt;
            } else {&lt;br /&gt;
                currentParam += char;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build expanded infobox&lt;br /&gt;
        var expanded = &#039;{{&#039; + templateName + &#039;\n&#039;;&lt;br /&gt;
        for (var p = 0; p &amp;lt; params.length; p++) {&lt;br /&gt;
            expanded += &#039;| &#039; + params[p] + &#039;\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        expanded += &#039;}}&#039;;&lt;br /&gt;
        &lt;br /&gt;
        return expanded;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            &lt;br /&gt;
            // Use sync versions for source mode&lt;br /&gt;
            var result;&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                result = autoAddLinksSync( sourceWikitext );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                result = fixAllSync( sourceWikitext );&lt;br /&gt;
            } else {&lt;br /&gt;
                result = fixCitations( sourceWikitext );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, use Parsoid but post-process to restore infobox formatting&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            &lt;br /&gt;
            // Helper to apply result to VE&lt;br /&gt;
            function applyResult( result ) {&lt;br /&gt;
                if ( result.wikitext === wikitext ) {&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Parse new wikitext through Parsoid&lt;br /&gt;
                veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                    veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                    &lt;br /&gt;
                    // After Parsoid, get the wikitext again and fix any collapsed infoboxes&lt;br /&gt;
                    setTimeout( function () {&lt;br /&gt;
                        target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( newWikitext ) {&lt;br /&gt;
                            var fixed = restoreInfoboxLinebreaks( newWikitext );&lt;br /&gt;
                            if ( fixed !== newWikitext ) {&lt;br /&gt;
                                veCreateDmDocumentFromWikitext( fixed, surface.getModel().getDocument() ).then( function ( fixedDoc ) {&lt;br /&gt;
                                    veReplaceAllContentWithDocument( surface, fixedDoc );&lt;br /&gt;
                                    result.fixes.push( &#039;Restored infobox formatting&#039; );&lt;br /&gt;
                                    showResultMessage( result );&lt;br /&gt;
                                } );&lt;br /&gt;
                            } else {&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            }&lt;br /&gt;
                        } );&lt;br /&gt;
                    }, 500 );&lt;br /&gt;
                }, function () {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Process based on mode - some functions are async&lt;br /&gt;
            if ( mode === &#039;links&#039; ) {&lt;br /&gt;
                autoAddLinks( wikitext ).then( applyResult );&lt;br /&gt;
            } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
                fixAll( wikitext ).then( applyResult );&lt;br /&gt;
            } else {&lt;br /&gt;
                applyResult( fixCitations( wikitext ) );&lt;br /&gt;
            }&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                autoAddLinks(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                mw.notify(&#039;Fetching linkify database...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                                fixAll(textarea.value).then(function(result) {&lt;br /&gt;
                                    textarea.value = result.wikitext;&lt;br /&gt;
                                    showResultMessage(result);&lt;br /&gt;
                                });&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            linksBtn.disabled = true;&lt;br /&gt;
            linksBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            autoAddLinks(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                linksBtn.disabled = false;&lt;br /&gt;
                linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            allBtn.disabled = true;&lt;br /&gt;
            allBtn.textContent = &#039;Loading...&#039;;&lt;br /&gt;
            fixAll(textbox.value).then(function(result) {&lt;br /&gt;
                textbox.value = result.wikitext;&lt;br /&gt;
                showResultMessage(result);&lt;br /&gt;
                allBtn.disabled = false;&lt;br /&gt;
                allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
            });&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        autoAddLinksSync: autoAddLinksSync,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        fixAllSync: fixAllSync,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        fetchLinkifyDatabase: fetchLinkifyDatabase,&lt;br /&gt;
        applyFormattingCleanup: applyFormattingCleanup,&lt;br /&gt;
        restoreInfoboxLinebreaks: restoreInfoboxLinebreaks&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3334</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3334"/>
		<updated>2026-01-05T10:43:44Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Parsoid with infobox linebreak restoration, preview refresh in source mode&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links (stub for future implementation)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Future implementation will:&lt;br /&gt;
        // - Detect unlinked character names and link them&lt;br /&gt;
        // - Detect unlinked chapter names and link them&lt;br /&gt;
        // - Add other automatic linking improvements&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        warnings.push(&#039;This feature will automatically add wiki links to character names, chapters, etc.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // Run Fix Citations first&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result&lt;br /&gt;
        var linkResult = autoAddLinks(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Restore linebreaks to collapsed infoboxes.&lt;br /&gt;
     * Parsoid may collapse {{Infobox ...|param=val|param2=val2}} onto one line.&lt;br /&gt;
     * This function expands them back to one parameter per line.&lt;br /&gt;
     */&lt;br /&gt;
    function restoreInfoboxLinebreaks(wikitext) {&lt;br /&gt;
        // Match infoboxes that are on a single line (collapsed)&lt;br /&gt;
        // Pattern: {{Infobox TYPE|param=val|param=val|...}}&lt;br /&gt;
        var infoboxPattern = /\{\{(Infobox[^|{}]*)\|([^{}]*(?:\{\{[^{}]*\}\}[^{}]*)*)\}\}/gi;&lt;br /&gt;
        &lt;br /&gt;
        return wikitext.replace(infoboxPattern, function(match, infoboxType, params) {&lt;br /&gt;
            // Check if already multi-line (has newlines between pipes)&lt;br /&gt;
            if (/\|\s*\n/.test(match)) {&lt;br /&gt;
                return match; // Already formatted, leave alone&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Split by | but be careful of nested templates and links&lt;br /&gt;
            var result = &#039;{{&#039; + infoboxType + &#039;\n&#039;;&lt;br /&gt;
            var depth = 0;&lt;br /&gt;
            var currentParam = &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            for (var i = 0; i &amp;lt; params.length; i++) {&lt;br /&gt;
                var char = params[i];&lt;br /&gt;
                &lt;br /&gt;
                if (char === &#039;{&#039; || char === &#039;[&#039;) {&lt;br /&gt;
                    depth++;&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                } else if (char === &#039;}&#039; || char === &#039;]&#039;) {&lt;br /&gt;
                    depth--;&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                } else if (char === &#039;|&#039; &amp;amp;&amp;amp; depth === 0) {&lt;br /&gt;
                    // End of parameter&lt;br /&gt;
                    if (currentParam.trim()) {&lt;br /&gt;
                        result += &#039;| &#039; + currentParam.trim() + &#039;\n&#039;;&lt;br /&gt;
                    }&lt;br /&gt;
                    currentParam = &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    currentParam += char;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Don&#039;t forget the last parameter&lt;br /&gt;
            if (currentParam.trim()) {&lt;br /&gt;
                result += &#039;| &#039; + currentParam.trim() + &#039;\n&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            result += &#039;}}&#039;;&lt;br /&gt;
            return result;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Select the appropriate processing function&lt;br /&gt;
        var processFunc;&lt;br /&gt;
        if ( mode === &#039;links&#039; ) {&lt;br /&gt;
            processFunc = autoAddLinks;&lt;br /&gt;
        } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
            processFunc = fixAll;&lt;br /&gt;
        } else {&lt;br /&gt;
            processFunc = fixCitations;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFunc( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
                // Trigger preview refresh&lt;br /&gt;
                setTimeout( function () {&lt;br /&gt;
                    if ( surface &amp;amp;&amp;amp; surface.getModel ) {&lt;br /&gt;
                        surface.getModel().emit( &#039;history&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }, 100 );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, use Parsoid but post-process to restore infobox formatting&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Parse new wikitext through Parsoid&lt;br /&gt;
            veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                &lt;br /&gt;
                // After Parsoid, get the wikitext again and fix any collapsed infoboxes&lt;br /&gt;
                setTimeout( function () {&lt;br /&gt;
                    // Re-serialize to check for collapsed infoboxes&lt;br /&gt;
                    target.getWikitextFragment( surface.getModel().getDocument() ).then( function ( newWikitext ) {&lt;br /&gt;
                        var fixed = restoreInfoboxLinebreaks( newWikitext );&lt;br /&gt;
                        if ( fixed !== newWikitext ) {&lt;br /&gt;
                            // Infoboxes were collapsed, need to re-apply with fixes&lt;br /&gt;
                            veCreateDmDocumentFromWikitext( fixed, surface.getModel().getDocument() ).then( function ( fixedDoc ) {&lt;br /&gt;
                                veReplaceAllContentWithDocument( surface, fixedDoc );&lt;br /&gt;
                                result.fixes.push( &#039;Restored infobox formatting&#039; );&lt;br /&gt;
                                showResultMessage( result );&lt;br /&gt;
                            } );&lt;br /&gt;
                        } else {&lt;br /&gt;
                            showResultMessage( result );&lt;br /&gt;
                        }&lt;br /&gt;
                    } );&lt;br /&gt;
                }, 500 );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                mw.notify( &#039;FormattingFixer: Could not apply changes. Try using source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3333</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3333"/>
		<updated>2026-01-05T10:37:58Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Surgical in-place ref updates in visual mode - no more source mode switching&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links (stub for future implementation)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Future implementation will:&lt;br /&gt;
        // - Detect unlinked character names and link them&lt;br /&gt;
        // - Detect unlinked chapter names and link them&lt;br /&gt;
        // - Add other automatic linking improvements&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        warnings.push(&#039;This feature will automatically add wiki links to character names, chapters, etc.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // Run Fix Citations first&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result&lt;br /&gt;
        var linkResult = autoAddLinks(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process a single ref&#039;s body content and return the fixed version&lt;br /&gt;
     */&lt;br /&gt;
    function processRefBody( bodyWikitext ) {&lt;br /&gt;
        if ( !bodyWikitext ) return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        &lt;br /&gt;
        var trimmed = bodyWikitext.trim();&lt;br /&gt;
        &lt;br /&gt;
        // Skip if already in Cite template&lt;br /&gt;
        if ( /\{\{Cite/i.test( trimmed ) ) {&lt;br /&gt;
            return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Only process chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if ( config.chapterUrlPattern.test( trimmed ) ) {&lt;br /&gt;
            var formatted = &#039;{{Cite chapter|url=&#039; + trimmed + &#039;}}&#039;;&lt;br /&gt;
            return { changed: true, wikitext: formatted };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { changed: false, wikitext: bodyWikitext };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Surgically update refs in VisualEditor without touching other content.&lt;br /&gt;
     * This modifies ref nodes directly in VE&#039;s internal list, avoiding Parsoid round-trip.&lt;br /&gt;
     */&lt;br /&gt;
    function veUpdateRefsInPlace( surface, mode ) {&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        var internalList = doc.getInternalList();&lt;br /&gt;
        var refGroups = internalList.getNodeGroups();&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var transactions = [];&lt;br /&gt;
&lt;br /&gt;
        // Iterate through all reference groups&lt;br /&gt;
        for ( var groupName in refGroups ) {&lt;br /&gt;
            if ( !refGroups.hasOwnProperty( groupName ) ) continue;&lt;br /&gt;
            if ( groupName.indexOf( &#039;mwReference/&#039; ) !== 0 ) continue;&lt;br /&gt;
&lt;br /&gt;
            var group = refGroups[ groupName ];&lt;br /&gt;
            var indexOrder = group.indexOrder;&lt;br /&gt;
&lt;br /&gt;
            for ( var i = 0; i &amp;lt; indexOrder.length; i++ ) {&lt;br /&gt;
                var itemIndex = indexOrder[ i ];&lt;br /&gt;
                var itemNode = internalList.getItemNode( itemIndex );&lt;br /&gt;
                if ( !itemNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the ref content node&lt;br /&gt;
                var refContentRange = itemNode.getRange();&lt;br /&gt;
                var refContent = doc.data.getText( refContentRange );&lt;br /&gt;
                &lt;br /&gt;
                // Also try to get the raw mw body if available&lt;br /&gt;
                var listNode = itemNode.children &amp;amp;&amp;amp; itemNode.children[ 0 ];&lt;br /&gt;
                if ( !listNode ) continue;&lt;br /&gt;
&lt;br /&gt;
                // Get the wikitext body from the mw attribute&lt;br /&gt;
                var mwData = null;&lt;br /&gt;
                try {&lt;br /&gt;
                    // Find the actual ref node that uses this internal list item&lt;br /&gt;
                    var nodes = group.firstNodes;&lt;br /&gt;
                    if ( nodes &amp;amp;&amp;amp; nodes[ i ] ) {&lt;br /&gt;
                        var refNode = nodes[ i ];&lt;br /&gt;
                        var refNodeData = doc.data.getData( refNode.getOffset() );&lt;br /&gt;
                        if ( refNodeData &amp;amp;&amp;amp; refNodeData.attributes &amp;amp;&amp;amp; refNodeData.attributes.mw ) {&lt;br /&gt;
                            mwData = refNodeData.attributes.mw;&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                } catch ( e ) {&lt;br /&gt;
                    // Ignore errors accessing node data&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Get body content - try mw.body.html first, then plain text&lt;br /&gt;
                var bodyWikitext = &#039;&#039;;&lt;br /&gt;
                if ( mwData &amp;amp;&amp;amp; mwData.body &amp;amp;&amp;amp; mwData.body.html ) {&lt;br /&gt;
                    // Parse HTML to get text content (simple case)&lt;br /&gt;
                    var tempDiv = document.createElement( &#039;div&#039; );&lt;br /&gt;
                    tempDiv.innerHTML = mwData.body.html;&lt;br /&gt;
                    bodyWikitext = tempDiv.textContent || tempDiv.innerText || &#039;&#039;;&lt;br /&gt;
                } else {&lt;br /&gt;
                    bodyWikitext = refContent;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                // Process this ref&lt;br /&gt;
                var result = processRefBody( bodyWikitext );&lt;br /&gt;
                if ( result.changed ) {&lt;br /&gt;
                    // Build a transaction to update this ref&#039;s content&lt;br /&gt;
                    // We need to update the mw attribute of the ref node&lt;br /&gt;
                    if ( mwData &amp;amp;&amp;amp; group.firstNodes &amp;amp;&amp;amp; group.firstNodes[ i ] ) {&lt;br /&gt;
                        var refNode = group.firstNodes[ i ];&lt;br /&gt;
                        var offset = refNode.getOffset();&lt;br /&gt;
                        &lt;br /&gt;
                        // Clone mwData and update body&lt;br /&gt;
                        var newMwData = ve.copy( mwData );&lt;br /&gt;
                        newMwData.body = { html: result.wikitext };&lt;br /&gt;
                        &lt;br /&gt;
                        // Create attribute change transaction&lt;br /&gt;
                        var tx = ve.dm.TransactionBuilder.static.newFromAttributeChanges(&lt;br /&gt;
                            doc,&lt;br /&gt;
                            offset,&lt;br /&gt;
                            { mw: newMwData }&lt;br /&gt;
                        );&lt;br /&gt;
                        transactions.push( tx );&lt;br /&gt;
                        fixes.push( &#039;Wrapped raw URL in {{Cite chapter}}&#039; );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Apply all transactions&lt;br /&gt;
        if ( transactions.length &amp;gt; 0 ) {&lt;br /&gt;
            var surfaceModel = surface.getModel();&lt;br /&gt;
            for ( var t = 0; t &amp;lt; transactions.length; t++ ) {&lt;br /&gt;
                surfaceModel.change( transactions[ t ] );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Also check for name generation on anonymous refs&lt;br /&gt;
        // (This is harder to do surgically, so we&#039;ll just warn)&lt;br /&gt;
        if ( mode !== &#039;links&#039; ) {&lt;br /&gt;
            // TODO: Implement surgical name assignment for anonymous refs&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings,&lt;br /&gt;
            changed: transactions.length &amp;gt; 0&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Use VE&#039;s built-in wikitext serialization and re-parse only changed refs&lt;br /&gt;
     */&lt;br /&gt;
    function veProcessRefsViaApi( surface, processFunc ) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        &lt;br /&gt;
        return target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            &lt;br /&gt;
            if ( result.wikitext === wikitext ) {&lt;br /&gt;
                // No changes needed&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return $.Deferred().resolve().promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Use VE&#039;s own mechanism to apply wikitext changes&lt;br /&gt;
            // This parses the new wikitext through the API but we&#039;re only&lt;br /&gt;
            // changing refs, not templates, so normalization should be minimal&lt;br /&gt;
            return veCreateDmDocumentFromWikitext( result.wikitext, doc ).then( function ( newDoc ) {&lt;br /&gt;
                veReplaceAllContentWithDocument( surface, newDoc );&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, function () {&lt;br /&gt;
                // If that fails, show results and offer copy option&lt;br /&gt;
                mw.notify( $( &#039;&amp;lt;div&amp;gt;&#039; ).append(&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Could not apply changes automatically.&#039; ),&lt;br /&gt;
                    $( &#039;&amp;lt;p&amp;gt;&#039; ).text( &#039;Fixes: &#039; + result.fixes.join( &#039;, &#039; ) ),&lt;br /&gt;
                    $( &#039;&amp;lt;button&amp;gt;&#039; ).text( &#039;Copy fixed wikitext&#039; ).on( &#039;click&#039;, function () {&lt;br /&gt;
                        navigator.clipboard.writeText( result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Copied!&#039; );&lt;br /&gt;
                    } )&lt;br /&gt;
                ), { autoHide: false, tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Select the appropriate processing function&lt;br /&gt;
        var processFunc;&lt;br /&gt;
        if ( mode === &#039;links&#039; ) {&lt;br /&gt;
            processFunc = autoAddLinks;&lt;br /&gt;
        } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
            processFunc = fixAll;&lt;br /&gt;
        } else {&lt;br /&gt;
            processFunc = fixCitations;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFunc( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, try surgical in-place updates first&lt;br /&gt;
        // This modifies only ref nodes without touching templates or file links&lt;br /&gt;
        try {&lt;br /&gt;
            var surgicalResult = veUpdateRefsInPlace( surface, mode );&lt;br /&gt;
            &lt;br /&gt;
            if ( surgicalResult.changed ) {&lt;br /&gt;
                showResultMessage( {&lt;br /&gt;
                    wikitext: &#039;&#039;,&lt;br /&gt;
                    fixes: surgicalResult.fixes,&lt;br /&gt;
                    warnings: [ &#039;Changes applied directly in Visual Editor.&#039; ]&lt;br /&gt;
                } );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // No surgical changes needed - show &amp;quot;all good&amp;quot; or check for other issues&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: No citation issues found in Visual Editor.&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
            &lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            console.log( &#039;FormattingFixer: Surgical update not available, using notification&#039;, e );&lt;br /&gt;
            // Surgical approach failed - just notify user to use source mode for full functionality&lt;br /&gt;
            mw.notify( &lt;br /&gt;
                &#039;FormattingFixer works best in source mode. Click &amp;quot;Edit source&amp;quot; to access all features.&#039;,&lt;br /&gt;
                { type: &#039;warn&#039;, tag: &#039;formattingfixer&#039; }&lt;br /&gt;
            );&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3332</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3332"/>
		<updated>2026-01-05T10:31:00Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update: narrow citation scope to chapter URLs only, add 3 menu options, dialog for visual mode&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only convert chapter URLs (must match /cXX/pXX pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // All other URLs are left unchanged&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only process if it&#039;s a chapter URL (matches /cXX/pXX)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto-add links (stub for future implementation)&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Future implementation will:&lt;br /&gt;
        // - Detect unlinked character names and link them&lt;br /&gt;
        // - Detect unlinked chapter names and link them&lt;br /&gt;
        // - Add other automatic linking improvements&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        warnings.push(&#039;This feature will automatically add wiki links to character names, chapters, etc.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // Run Fix Citations first&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result&lt;br /&gt;
        var linkResult = autoAddLinks(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show a dialog for visual mode with options&lt;br /&gt;
     */&lt;br /&gt;
    function showVisualModeDialog( originalWikitext, result, processFunc ) {&lt;br /&gt;
        if ( result.wikitext === originalWikitext &amp;amp;&amp;amp; result.fixes.length === 0 ) {&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Build message&lt;br /&gt;
        var changesText = &#039;&#039;;&lt;br /&gt;
        if ( result.fixes.length &amp;gt; 0 ) {&lt;br /&gt;
            changesText = &#039;Changes to apply:\\n&#039; + result.fixes.join( &#039;\\n&#039; );&lt;br /&gt;
        }&lt;br /&gt;
        if ( result.warnings.length &amp;gt; 0 ) {&lt;br /&gt;
            if ( changesText ) changesText += &#039;\\n\\n&#039;;&lt;br /&gt;
            changesText += result.warnings.join( &#039;\\n&#039; );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Use OO.ui dialog if available&lt;br /&gt;
        if ( typeof OO !== &#039;undefined&#039; &amp;amp;&amp;amp; OO.ui &amp;amp;&amp;amp; OO.ui.MessageDialog ) {&lt;br /&gt;
            var dialog = new OO.ui.MessageDialog();&lt;br /&gt;
            var windowManager = new OO.ui.WindowManager();&lt;br /&gt;
            $( document.body ).append( windowManager.$element );&lt;br /&gt;
            windowManager.addWindows( [ dialog ] );&lt;br /&gt;
&lt;br /&gt;
            windowManager.openWindow( dialog, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                message: $( &#039;&amp;lt;div&amp;gt;&#039; ).css( &#039;white-space&#039;, &#039;pre-wrap&#039; ).text( changesText || &#039;No changes needed.&#039; ),&lt;br /&gt;
                actions: [&lt;br /&gt;
                    { action: &#039;apply&#039;, label: &#039;Switch to Source &amp;amp; Apply&#039;, flags: [ &#039;primary&#039;, &#039;progressive&#039; ] },&lt;br /&gt;
                    { action: &#039;copy&#039;, label: &#039;Copy Fixed Wikitext&#039; },&lt;br /&gt;
                    { action: &#039;cancel&#039;, label: &#039;Cancel&#039;, flags: &#039;safe&#039; }&lt;br /&gt;
                ]&lt;br /&gt;
            } ).closed.then( function ( data ) {&lt;br /&gt;
                windowManager.destroy();&lt;br /&gt;
                if ( !data || !data.action ) return;&lt;br /&gt;
&lt;br /&gt;
                if ( data.action === &#039;copy&#039; ) {&lt;br /&gt;
                    navigator.clipboard.writeText( result.wikitext ).then( function () {&lt;br /&gt;
                        mw.notify( &#039;Fixed wikitext copied to clipboard!&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
                    }, function () {&lt;br /&gt;
                        // Fallback: show in prompt&lt;br /&gt;
                        prompt( &#039;Copy this wikitext:&#039;, result.wikitext );&lt;br /&gt;
                    } );&lt;br /&gt;
                } else if ( data.action === &#039;apply&#039; ) {&lt;br /&gt;
                    veEnsureSourceMode().then( function () {&lt;br /&gt;
                        var newSurface = veGetSurface();&lt;br /&gt;
                        if ( newSurface &amp;amp;&amp;amp; veGetMode() === &#039;source&#039; ) {&lt;br /&gt;
                            veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
                            mw.notify( &#039;Changes applied!&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
                        }&lt;br /&gt;
                    } );&lt;br /&gt;
                }&lt;br /&gt;
            } );&lt;br /&gt;
        } else {&lt;br /&gt;
            // Fallback to confirm/alert&lt;br /&gt;
            var doApply = confirm( changesText + &#039;\\n\\nClick OK to switch to source mode and apply changes.&#039; );&lt;br /&gt;
            if ( doApply ) {&lt;br /&gt;
                veEnsureSourceMode().then( function () {&lt;br /&gt;
                    var newSurface = veGetSurface();&lt;br /&gt;
                    if ( newSurface &amp;amp;&amp;amp; veGetMode() === &#039;source&#039; ) {&lt;br /&gt;
                        veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
                        mw.notify( &#039;Changes applied!&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
                    }&lt;br /&gt;
                } );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Select the appropriate processing function&lt;br /&gt;
        var processFunc;&lt;br /&gt;
        if ( mode === &#039;links&#039; ) {&lt;br /&gt;
            processFunc = autoAddLinks;&lt;br /&gt;
        } else if ( mode === &#039;all&#039; ) {&lt;br /&gt;
            processFunc = fixAll;&lt;br /&gt;
        } else {&lt;br /&gt;
            processFunc = fixCitations;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFunc( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, serialize to wikitext, process, then show dialog with options&lt;br /&gt;
        var doc = surface.getModel().getDocument();&lt;br /&gt;
        target.getWikitextFragment( doc ).then( function ( wikitext ) {&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            showVisualModeDialog( wikitext, result, processFunc );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not get wikitext from visual editor.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;wikiText&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;wikiText&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #36c; color: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3330</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3330"/>
		<updated>2026-01-05T10:18:47Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Narrow citation wrapping to chapter URLs only; add 3 buttons: Fix Citations, Auto Add Links (stub), Fix All&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only wrap chapter URLs (must match /cNUMBER pattern)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t wrap other site URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY wrap chapter URLs (must match /cNUMBER/pNUMBER pattern)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var formatted = formatCitation(trimmedContent);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links - stub for future implementation&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add [[wikilinks]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        warnings.push(&#039;This feature will automatically add [[wikilinks]] to recognized terms.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run both Fix Citations and Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // Run Fix Citations first&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        &lt;br /&gt;
        // Then run Auto Add Links on the result&lt;br /&gt;
        var linkResult = autoAddLinks(citationResult.wikitext);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: linkResult.wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( mode ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
        &lt;br /&gt;
        // Select the appropriate function based on mode&lt;br /&gt;
        var processFunc = mode === &#039;links&#039; ? autoAddLinks : &lt;br /&gt;
                          mode === &#039;all&#039; ? fixAll : fixCitations;&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFunc( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        // which would collapse multiline templates and remove underscores from filenames&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            var newSurface = veGetSurface();&lt;br /&gt;
            if ( !newSurface || veGetMode() !== &#039;source&#039; ) {&lt;br /&gt;
                mw.notify( &#039;FormattingFixer: Could not switch to source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
            var result = processFunc( wikitext );&lt;br /&gt;
            if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;references&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;citations&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix Citations + Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;all&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;references&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;📚 Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;🔗 Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;✨ Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3329</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3329"/>
		<updated>2026-01-05T10:06:41Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: FormattingFixer: nicer notify + preview refresh; chapter-only citation; add Links actions (stub)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    function getNotifyType( result ) {&lt;br /&gt;
        return result &amp;amp;&amp;amp; result.warnings &amp;amp;&amp;amp; result.warnings.length ? &#039;warn&#039; : &#039;success&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Check if it&#039;s a chapter URL&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var changed = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
            changed++;&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes, changed: changed };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        var changed = 0;&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Only standardize proper chapter/page URLs&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changed++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, changed: changed };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            changeCount += orderIssues.length;&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var citationResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (citationResult.wikitext !== wikitext) {&lt;br /&gt;
            wikitext = citationResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized raw chapter URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            changeCount += citationResult.changed;&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            changeCount += namingResult.changed;&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function autoAddLinks( wikitext ) {&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: [],&lt;br /&gt;
            warnings: []&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runActionOnWikitext( wikitext, action ) {&lt;br /&gt;
        var result;&lt;br /&gt;
        var combinedFixes = [];&lt;br /&gt;
        var combinedWarnings = [];&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
&lt;br /&gt;
        if ( action === &#039;links&#039; ) {&lt;br /&gt;
            result = autoAddLinks( wikitext );&lt;br /&gt;
        } else if ( action === &#039;fixlinks&#039; ) {&lt;br /&gt;
            result = fixCitations( wikitext );&lt;br /&gt;
            combinedFixes = combinedFixes.concat( result.fixes || [] );&lt;br /&gt;
            combinedWarnings = combinedWarnings.concat( result.warnings || [] );&lt;br /&gt;
            wikitext = result.wikitext;&lt;br /&gt;
            result = autoAddLinks( wikitext );&lt;br /&gt;
            combinedFixes = combinedFixes.concat( result.fixes || [] );&lt;br /&gt;
            combinedWarnings = combinedWarnings.concat( result.warnings || [] );&lt;br /&gt;
            result = { wikitext: result.wikitext, fixes: combinedFixes, warnings: combinedWarnings };&lt;br /&gt;
        } else {&lt;br /&gt;
            result = fixCitations( wikitext );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        result._changed = ( result.wikitext !== before );&lt;br /&gt;
        return result;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var fixCount = (result &amp;amp;&amp;amp; result.fixes) ? result.fixes.length : 0;&lt;br /&gt;
        var warnCount = (result &amp;amp;&amp;amp; result.warnings) ? result.warnings.length : 0;&lt;br /&gt;
        var summary;&lt;br /&gt;
&lt;br /&gt;
        if ( fixCount === 0 &amp;amp;&amp;amp; warnCount === 0 ) {&lt;br /&gt;
            summary = &#039;No changes needed.&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
            summary = &#039;Applied &#039; + fixCount + &#039; change(s)&#039;;&lt;br /&gt;
            if ( warnCount ) {&lt;br /&gt;
                summary += &#039;; &#039; + warnCount + &#039; warning(s)&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            summary += &#039;.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if ( result &amp;amp;&amp;amp; result.fixes &amp;amp;&amp;amp; result.fixes.length ) {&lt;br /&gt;
            console.log( &#039;FormattingFixer fixes:&#039;, result.fixes );&lt;br /&gt;
        }&lt;br /&gt;
        if ( result &amp;amp;&amp;amp; result.warnings &amp;amp;&amp;amp; result.warnings.length ) {&lt;br /&gt;
            console.warn( &#039;FormattingFixer warnings:&#039;, result.warnings );&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify( summary, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                autoHide: true,&lt;br /&gt;
                type: getNotifyType( result ),&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            } );&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(summary);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE( action ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = runActionOnWikitext( sourceWikitext, action );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        // which would collapse multiline templates and remove underscores from filenames&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            var newSurface = veGetSurface();&lt;br /&gt;
            if ( !newSurface || veGetMode() !== &#039;source&#039; ) {&lt;br /&gt;
                mw.notify( &#039;FormattingFixer: Could not switch to source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
            var result = runActionOnWikitext( wikitext, action );&lt;br /&gt;
            if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;fix&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;links&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFixLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixLinksTool() {&lt;br /&gt;
                    FormattingFixerFixLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixLinksTool.static.name = &#039;formattingFixerFixLinks&#039;;&lt;br /&gt;
                FormattingFixerFixLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixLinksTool.static.title = &#039;Fix Citations and Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerFixLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerFixLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE( &#039;fixlinks&#039; );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerFixLinks&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = runActionOnWikitext( textarea.value, &#039;fix&#039; );&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                                    $( textarea ).trigger( &#039;input&#039; ).trigger( &#039;change&#039; );&lt;br /&gt;
                                }&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = runActionOnWikitext( textarea.value, &#039;links&#039; );&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                                    $( textarea ).trigger( &#039;input&#039; ).trigger( &#039;change&#039; );&lt;br /&gt;
                                }&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-fixlinks&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations + Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = runActionOnWikitext( textarea.value, &#039;fixlinks&#039; );&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                                    $( textarea ).trigger( &#039;input&#039; ).trigger( &#039;change&#039; );&lt;br /&gt;
                                }&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor button added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = runActionOnWikitext( textbox.value, &#039;fix&#039; );&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                $( textbox ).trigger( &#039;input&#039; ).trigger( &#039;change&#039; );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = runActionOnWikitext( textbox.value, &#039;links&#039; );&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                $( textbox ).trigger( &#039;input&#039; ).trigger( &#039;change&#039; );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var bothBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        bothBtn.textContent = &#039;Fix Citations + Links&#039;;&lt;br /&gt;
        bothBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        bothBtn.type = &#039;button&#039;;&lt;br /&gt;
        bothBtn.onclick = function() {&lt;br /&gt;
            var result = runActionOnWikitext( textbox.value, &#039;fixlinks&#039; );&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                $( textbox ).trigger( &#039;input&#039; ).trigger( &#039;change&#039; );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(bothBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName,&lt;br /&gt;
        autoAddLinks: autoAddLinks&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3328</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3328"/>
		<updated>2026-01-05T09:54:02Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix VE switch flakiness: poll for wpTextbox1 and apply regardless of promise rejection&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            var switchPromise = target.switchToWikitextEditor( true );&lt;br /&gt;
            // If switchToWikitextEditor returns undefined, create our own promise&lt;br /&gt;
            if ( !switchPromise || typeof switchPromise.then !== &#039;function&#039; ) {&lt;br /&gt;
                var deferred = $.Deferred();&lt;br /&gt;
                var attempt = 0;&lt;br /&gt;
                var check = function () {&lt;br /&gt;
                    attempt++;&lt;br /&gt;
                    var newSurface = veGetSurface();&lt;br /&gt;
                    if ( newSurface &amp;amp;&amp;amp; veGetMode() === &#039;source&#039; ) {&lt;br /&gt;
                        deferred.resolve();&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if ( attempt &amp;gt;= 20 ) {&lt;br /&gt;
                        deferred.reject( new Error( &#039;Mode switch failed&#039; ) );&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    setTimeout( check, 200 );&lt;br /&gt;
                };&lt;br /&gt;
                // Poll for up to ~4 seconds&lt;br /&gt;
                setTimeout( check, 0 );&lt;br /&gt;
                return deferred.promise();&lt;br /&gt;
            }&lt;br /&gt;
            return switchPromise;&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var textbox = document.getElementById( &#039;wpTextbox1&#039; );&lt;br /&gt;
            if ( textbox &amp;amp;&amp;amp; !( textbox.classList &amp;amp;&amp;amp; textbox.classList.contains( &#039;ve-dummyTextbox&#039; ) ) ) {&lt;br /&gt;
                var sourceWikitext = textbox.value;&lt;br /&gt;
                var result = processFn( sourceWikitext );&lt;br /&gt;
                if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                    textbox.value = result.wikitext;&lt;br /&gt;
                    if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                        $( textbox ).trigger( &#039;input&#039; );&lt;br /&gt;
                    } else {&lt;br /&gt;
                        textbox.dispatchEvent( new Event( &#039;input&#039;, { bubbles: true } ) );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Fallback if #wpTextbox1 is not present&lt;br /&gt;
            var sourceSurfaceText = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var fallbackResult = processFn( sourceSurfaceText );&lt;br /&gt;
            if ( fallbackResult.wikitext !== sourceSurfaceText ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, fallbackResult.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( fallbackResult );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
&lt;br /&gt;
        // Request switch, but do not rely on promise resolution (it can reject even&lt;br /&gt;
        // when the UI switches successfully on some installs).&lt;br /&gt;
        try {&lt;br /&gt;
            if ( target &amp;amp;&amp;amp; target.switchToWikitextEditor ) {&lt;br /&gt;
                target.switchToWikitextEditor( true );&lt;br /&gt;
            }&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            // Keep going; polling below will still work if the switch happens anyway.&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // After switching, apply edits to the real wikitext textarea.&lt;br /&gt;
        // This is more reliable than VE document-model APIs for source mode.&lt;br /&gt;
        var attemptApplyToTextbox = function ( attempt ) {&lt;br /&gt;
            attempt = attempt || 1;&lt;br /&gt;
            var textbox = document.getElementById( &#039;wpTextbox1&#039; );&lt;br /&gt;
            if ( textbox &amp;amp;&amp;amp; !( textbox.classList &amp;amp;&amp;amp; textbox.classList.contains( &#039;ve-dummyTextbox&#039; ) ) ) {&lt;br /&gt;
                var wikitext = textbox.value;&lt;br /&gt;
                var result = processFn( wikitext );&lt;br /&gt;
                if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                    textbox.value = result.wikitext;&lt;br /&gt;
                    if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                        $( textbox ).trigger( &#039;input&#039; );&lt;br /&gt;
                    } else {&lt;br /&gt;
                        textbox.dispatchEvent( new Event( &#039;input&#039;, { bubbles: true } ) );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            if ( attempt &amp;lt; 20 ) {&lt;br /&gt;
                setTimeout( function () { attemptApplyToTextbox( attempt + 1 ); }, 200 );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Switched to source mode, but could not access the wikitext textarea.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        attemptApplyToTextbox( 1 );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3327</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3327"/>
		<updated>2026-01-05T09:45:50Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix VE visual-mode apply: write to #wpTextbox1 after switching to source mode&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            var switchPromise = target.switchToWikitextEditor( true );&lt;br /&gt;
            // If switchToWikitextEditor returns undefined, create our own promise&lt;br /&gt;
            if ( !switchPromise || typeof switchPromise.then !== &#039;function&#039; ) {&lt;br /&gt;
                var deferred = $.Deferred();&lt;br /&gt;
                // Wait a bit and check if the switch worked&lt;br /&gt;
                setTimeout( function() {&lt;br /&gt;
                    var newSurface = veGetSurface();&lt;br /&gt;
                    if ( newSurface &amp;amp;&amp;amp; veGetMode() === &#039;source&#039; ) {&lt;br /&gt;
                        deferred.resolve();&lt;br /&gt;
                    } else {&lt;br /&gt;
                        deferred.reject( new Error( &#039;Mode switch failed&#039; ) );&lt;br /&gt;
                    }&lt;br /&gt;
                }, 500 );&lt;br /&gt;
                return deferred.promise();&lt;br /&gt;
            }&lt;br /&gt;
            return switchPromise;&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var textbox = document.getElementById( &#039;wpTextbox1&#039; );&lt;br /&gt;
            if ( textbox &amp;amp;&amp;amp; !( textbox.classList &amp;amp;&amp;amp; textbox.classList.contains( &#039;ve-dummyTextbox&#039; ) ) ) {&lt;br /&gt;
                var sourceWikitext = textbox.value;&lt;br /&gt;
                var result = processFn( sourceWikitext );&lt;br /&gt;
                if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                    textbox.value = result.wikitext;&lt;br /&gt;
                    if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                        $( textbox ).trigger( &#039;input&#039; );&lt;br /&gt;
                    } else {&lt;br /&gt;
                        textbox.dispatchEvent( new Event( &#039;input&#039;, { bubbles: true } ) );&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Fallback if #wpTextbox1 is not present&lt;br /&gt;
            var sourceSurfaceText = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var fallbackResult = processFn( sourceSurfaceText );&lt;br /&gt;
            if ( fallbackResult.wikitext !== sourceSurfaceText ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, fallbackResult.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( fallbackResult );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            // After switching, apply edits to the real wikitext textarea.&lt;br /&gt;
            // This is more reliable than VE document-model APIs for source mode.&lt;br /&gt;
            var attemptApplyToTextbox = function ( attempt ) {&lt;br /&gt;
                attempt = attempt || 1;&lt;br /&gt;
                var textbox = document.getElementById( &#039;wpTextbox1&#039; );&lt;br /&gt;
                if ( textbox &amp;amp;&amp;amp; !( textbox.classList &amp;amp;&amp;amp; textbox.classList.contains( &#039;ve-dummyTextbox&#039; ) ) ) {&lt;br /&gt;
                    var wikitext = textbox.value;&lt;br /&gt;
                    var result = processFn( wikitext );&lt;br /&gt;
                    if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                        textbox.value = result.wikitext;&lt;br /&gt;
                        if ( typeof $ !== &#039;undefined&#039; ) {&lt;br /&gt;
                            $( textbox ).trigger( &#039;input&#039; );&lt;br /&gt;
                        } else {&lt;br /&gt;
                            textbox.dispatchEvent( new Event( &#039;input&#039;, { bubbles: true } ) );&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                    showResultMessage( result );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                if ( attempt &amp;lt; 10 ) {&lt;br /&gt;
                    setTimeout( function () { attemptApplyToTextbox( attempt + 1 ); }, 200 );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.notify( &#039;FormattingFixer: Switched to source mode, but could not access the wikitext textarea.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            attemptApplyToTextbox( 1 );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3326</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3326"/>
		<updated>2026-01-05T09:37:45Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix veEnsureSourceMode to handle undefined return from switchToWikitextEditor&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            var switchPromise = target.switchToWikitextEditor( true );&lt;br /&gt;
            // If switchToWikitextEditor returns undefined, create our own promise&lt;br /&gt;
            if ( !switchPromise || typeof switchPromise.then !== &#039;function&#039; ) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: switchToWikitextEditor returned undefined, creating manual promise&#039;);&lt;br /&gt;
                var deferred = $.Deferred();&lt;br /&gt;
                // Wait a bit and check if the switch worked&lt;br /&gt;
                setTimeout( function() {&lt;br /&gt;
                    var newSurface = veGetSurface();&lt;br /&gt;
                    if ( newSurface &amp;amp;&amp;amp; veGetMode() === &#039;source&#039; ) {&lt;br /&gt;
                        deferred.resolve();&lt;br /&gt;
                    } else {&lt;br /&gt;
                        deferred.reject( new Error( &#039;Mode switch failed&#039; ) );&lt;br /&gt;
                    }&lt;br /&gt;
                }, 500 );&lt;br /&gt;
                return deferred.promise();&lt;br /&gt;
            }&lt;br /&gt;
            return switchPromise;&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFn( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            // Wait longer for the mode switch to complete and use VE event&lt;br /&gt;
            var attemptProcessing = function( attempt ) {&lt;br /&gt;
                attempt = attempt || 1;&lt;br /&gt;
                var newSurface = veGetSurface();&lt;br /&gt;
                var currentMode = veGetMode();&lt;br /&gt;
                &lt;br /&gt;
                console.log(&#039;FormattingFixer: Attempt&#039;, attempt, &#039;Mode:&#039;, currentMode, &#039;Surface:&#039;, !!newSurface);&lt;br /&gt;
                &lt;br /&gt;
                if ( !newSurface || currentMode !== &#039;source&#039; ) {&lt;br /&gt;
                    if ( attempt &amp;lt; 10 ) {&lt;br /&gt;
                        // Try again in 200ms, up to 10 times (2 seconds total)&lt;br /&gt;
                        setTimeout( function() { attemptProcessing( attempt + 1 ); }, 200 );&lt;br /&gt;
                        return;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        mw.notify( &#039;FormattingFixer: Could not switch to source mode after 2 seconds.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Successfully in source mode, now process&lt;br /&gt;
                console.log(&#039;FormattingFixer: Successfully in source mode, processing...&#039;);&lt;br /&gt;
                var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Got wikitext, length:&#039;, wikitext.length);&lt;br /&gt;
                var result = processFn( wikitext );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Processing result:&#039;, result.fixes.length, &#039;fixes,&#039;, result.warnings.length, &#039;warnings&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: Applying changes...&#039;);&lt;br /&gt;
                    veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
                } else {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: No changes needed&#039;);&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            };&lt;br /&gt;
            &lt;br /&gt;
            attemptProcessing();&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: VE not available for tool registration&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            console.log(&#039;FormattingFixer: Registering VE tools...&#039;);&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: Fix Citations button clicked in VE&#039;);&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Registered Fix Citations tool&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                console.log(&#039;FormattingFixer: Fix Citations tool already exists&#039;);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3325</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3325"/>
		<updated>2026-01-05T09:36:28Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add debug logging to VE tool registration and button clicks&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFn( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            // Wait longer for the mode switch to complete and use VE event&lt;br /&gt;
            var attemptProcessing = function( attempt ) {&lt;br /&gt;
                attempt = attempt || 1;&lt;br /&gt;
                var newSurface = veGetSurface();&lt;br /&gt;
                var currentMode = veGetMode();&lt;br /&gt;
                &lt;br /&gt;
                console.log(&#039;FormattingFixer: Attempt&#039;, attempt, &#039;Mode:&#039;, currentMode, &#039;Surface:&#039;, !!newSurface);&lt;br /&gt;
                &lt;br /&gt;
                if ( !newSurface || currentMode !== &#039;source&#039; ) {&lt;br /&gt;
                    if ( attempt &amp;lt; 10 ) {&lt;br /&gt;
                        // Try again in 200ms, up to 10 times (2 seconds total)&lt;br /&gt;
                        setTimeout( function() { attemptProcessing( attempt + 1 ); }, 200 );&lt;br /&gt;
                        return;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        mw.notify( &#039;FormattingFixer: Could not switch to source mode after 2 seconds.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Successfully in source mode, now process&lt;br /&gt;
                console.log(&#039;FormattingFixer: Successfully in source mode, processing...&#039;);&lt;br /&gt;
                var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Got wikitext, length:&#039;, wikitext.length);&lt;br /&gt;
                var result = processFn( wikitext );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Processing result:&#039;, result.fixes.length, &#039;fixes,&#039;, result.warnings.length, &#039;warnings&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: Applying changes...&#039;);&lt;br /&gt;
                    veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
                } else {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: No changes needed&#039;);&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            };&lt;br /&gt;
            &lt;br /&gt;
            attemptProcessing();&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: VE not available for tool registration&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            console.log(&#039;FormattingFixer: Registering VE tools...&#039;);&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: Fix Citations button clicked in VE&#039;);&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Registered Fix Citations tool&#039;);&lt;br /&gt;
            } else {&lt;br /&gt;
                console.log(&#039;FormattingFixer: Fix Citations tool already exists&#039;);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3324</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3324"/>
		<updated>2026-01-05T09:34:15Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add retry logic and console debugging for VE mode switch timing issues&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFn( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            // Wait longer for the mode switch to complete and use VE event&lt;br /&gt;
            var attemptProcessing = function( attempt ) {&lt;br /&gt;
                attempt = attempt || 1;&lt;br /&gt;
                var newSurface = veGetSurface();&lt;br /&gt;
                var currentMode = veGetMode();&lt;br /&gt;
                &lt;br /&gt;
                console.log(&#039;FormattingFixer: Attempt&#039;, attempt, &#039;Mode:&#039;, currentMode, &#039;Surface:&#039;, !!newSurface);&lt;br /&gt;
                &lt;br /&gt;
                if ( !newSurface || currentMode !== &#039;source&#039; ) {&lt;br /&gt;
                    if ( attempt &amp;lt; 10 ) {&lt;br /&gt;
                        // Try again in 200ms, up to 10 times (2 seconds total)&lt;br /&gt;
                        setTimeout( function() { attemptProcessing( attempt + 1 ); }, 200 );&lt;br /&gt;
                        return;&lt;br /&gt;
                    } else {&lt;br /&gt;
                        mw.notify( &#039;FormattingFixer: Could not switch to source mode after 2 seconds.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                // Successfully in source mode, now process&lt;br /&gt;
                console.log(&#039;FormattingFixer: Successfully in source mode, processing...&#039;);&lt;br /&gt;
                var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Got wikitext, length:&#039;, wikitext.length);&lt;br /&gt;
                var result = processFn( wikitext );&lt;br /&gt;
                console.log(&#039;FormattingFixer: Processing result:&#039;, result.fixes.length, &#039;fixes,&#039;, result.warnings.length, &#039;warnings&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: Applying changes...&#039;);&lt;br /&gt;
                    veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
                } else {&lt;br /&gt;
                    console.log(&#039;FormattingFixer: No changes needed&#039;);&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            };&lt;br /&gt;
            &lt;br /&gt;
            attemptProcessing();&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3323</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3323"/>
		<updated>2026-01-05T09:29:26Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Fix Visual Editor mode switch timing - add 100ms delay after switching to source mode&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFn( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            // Add a small delay to ensure the mode switch is complete&lt;br /&gt;
            setTimeout( function () {&lt;br /&gt;
                var newSurface = veGetSurface();&lt;br /&gt;
                if ( !newSurface || veGetMode() !== &#039;source&#039; ) {&lt;br /&gt;
                    mw.notify( &#039;FormattingFixer: Could not switch to source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
                var result = processFn( wikitext );&lt;br /&gt;
                if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                    veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
                }&lt;br /&gt;
                showResultMessage( result );&lt;br /&gt;
            }, 100 );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3322</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3322"/>
		<updated>2026-01-05T09:18:04Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Improve toast formatting, narrow URL matching to chapter URLs only, add 3 buttons (Fix Citations, Auto Add Links stub, Fix All)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Only format chapter URLs (must start with /c followed by number)&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Don&#039;t touch other URLs (like /img/, /store/, etc.)&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format - ONLY for chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var changeCount = 0;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // ONLY process chapter URLs (must have /cNUMBER/pNUMBER)&lt;br /&gt;
            if (config.chapterUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                changeCount++;&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, count: changeCount };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format (only chapter URLs)&lt;br /&gt;
        var standardizeResult = standardizeCitations(wikitext);&lt;br /&gt;
        if (standardizeResult.count &amp;gt; 0) {&lt;br /&gt;
            wikitext = standardizeResult.wikitext;&lt;br /&gt;
            fixes.push(&#039;Standardized &#039; + standardizeResult.count + &#039; raw URL(s) to {{Cite chapter}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify with clean summary&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var $content;&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            mw.notify(&#039;No issues found! Citations look good.&#039;, {&lt;br /&gt;
                title: &#039;FormattingFixer&#039;,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Build a clean jQuery element for the notification&lt;br /&gt;
        $content = $(&#039;&amp;lt;div&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).text(result.fixes.length + &#039; fix(es) applied&#039;));&lt;br /&gt;
            var $fixList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 10px 20px&#039;, padding: 0 });&lt;br /&gt;
            result.fixes.forEach(function(fix) {&lt;br /&gt;
                $fixList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(fix));&lt;br /&gt;
            });&lt;br /&gt;
            $content.append($fixList);&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            // Filter out empty warnings&lt;br /&gt;
            var realWarnings = result.warnings.filter(function(w) { return w.trim() !== &#039;&#039; &amp;amp;&amp;amp; !w.startsWith(&#039;===&#039;); });&lt;br /&gt;
            if (realWarnings.length &amp;gt; 0) {&lt;br /&gt;
                $content.append($(&#039;&amp;lt;strong&amp;gt;&#039;).css(&#039;color&#039;, &#039;#d33&#039;).text(realWarnings.length + &#039; warning(s)&#039;));&lt;br /&gt;
                var $warnList = $(&#039;&amp;lt;ul&amp;gt;&#039;).css({ margin: &#039;5px 0 0 20px&#039;, padding: 0 });&lt;br /&gt;
                realWarnings.forEach(function(warn) {&lt;br /&gt;
                    $warnList.append($(&#039;&amp;lt;li&amp;gt;&#039;).text(warn));&lt;br /&gt;
                });&lt;br /&gt;
                $content.append($warnList);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        mw.notify($content, {&lt;br /&gt;
            title: &#039;FormattingFixer&#039;,&lt;br /&gt;
            autoHide: false,&lt;br /&gt;
            tag: &#039;formattingfixer&#039;&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Auto Add Links stub - will be implemented later&lt;br /&gt;
     */&lt;br /&gt;
    function autoAddLinks(wikitext) {&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        &lt;br /&gt;
        // TODO: Implement auto-linking logic&lt;br /&gt;
        // This will scan for unlinked character names, chapter titles, etc.&lt;br /&gt;
        // and automatically add wiki links [[Name]] around them.&lt;br /&gt;
        &lt;br /&gt;
        warnings.push(&#039;Auto Add Links is not yet implemented.&#039;);&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Combined function: Fix Citations + Auto Add Links&lt;br /&gt;
     */&lt;br /&gt;
    function fixAll(wikitext) {&lt;br /&gt;
        // First fix citations&lt;br /&gt;
        var citationResult = fixCitations(wikitext);&lt;br /&gt;
        wikitext = citationResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Then auto add links&lt;br /&gt;
        var linkResult = autoAddLinks(wikitext);&lt;br /&gt;
        wikitext = linkResult.wikitext;&lt;br /&gt;
        &lt;br /&gt;
        // Combine results&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: citationResult.fixes.concat(linkResult.fixes),&lt;br /&gt;
            warnings: citationResult.warnings.concat(linkResult.warnings)&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generic VE runner for any processing function&lt;br /&gt;
     */&lt;br /&gt;
    function runInVE( processFn ) {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = processFn( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            var newSurface = veGetSurface();&lt;br /&gt;
            if ( !newSurface || veGetMode() !== &#039;source&#039; ) {&lt;br /&gt;
                mw.notify( &#039;FormattingFixer: Could not switch to source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
            var result = processFn( wikitext );&lt;br /&gt;
            if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            // Tool 1: Fix Citations&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;reference&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixCitations );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 2: Auto Add Links&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerLinks&#039; ) ) {&lt;br /&gt;
                function FormattingFixerLinksTool() {&lt;br /&gt;
                    FormattingFixerLinksTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerLinksTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerLinksTool.static.name = &#039;formattingFixerLinks&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.title = &#039;Auto Add Links&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerLinksTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( autoAddLinks );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerLinksTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerLinksTool );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Tool 3: Fix All (Citations + Links)&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerAll&#039; ) ) {&lt;br /&gt;
                function FormattingFixerAllTool() {&lt;br /&gt;
                    FormattingFixerAllTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerAllTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerAllTool.static.name = &#039;formattingFixerAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.title = &#039;Fix All&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.icon = &#039;checkAll&#039;;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerAllTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerAllTool.prototype.onSelect = function () {&lt;br /&gt;
                    runInVE( fixAll );&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerAllTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerAllTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039;, &#039;formattingFixerLinks&#039;, &#039;formattingFixerAll&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;reference&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                // Trigger preview refresh if available&lt;br /&gt;
                                if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $(&#039;#wpPreview&#039;).length) {&lt;br /&gt;
                                    // Signal that content changed for live preview&lt;br /&gt;
                                    $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-links&#039;: {&lt;br /&gt;
                        label: &#039;Auto Add Links&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;link&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = autoAddLinks(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-all&#039;: {&lt;br /&gt;
                        label: &#039;Fix All&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;checkAll&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixAll(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                                $(textarea).trigger(&#039;input&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var btnStyle = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = btnStyle;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var linksBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        linksBtn.textContent = &#039;Auto Add Links&#039;;&lt;br /&gt;
        linksBtn.style.cssText = btnStyle;&lt;br /&gt;
        linksBtn.type = &#039;button&#039;;&lt;br /&gt;
        linksBtn.onclick = function() {&lt;br /&gt;
            var result = autoAddLinks(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(linksBtn);&lt;br /&gt;
&lt;br /&gt;
        var allBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        allBtn.textContent = &#039;Fix All&#039;;&lt;br /&gt;
        allBtn.style.cssText = btnStyle;&lt;br /&gt;
        allBtn.type = &#039;button&#039;;&lt;br /&gt;
        allBtn.onclick = function() {&lt;br /&gt;
            var result = fixAll(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(allBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        autoAddLinks: autoAddLinks,&lt;br /&gt;
        fixAll: fixAll,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3321</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3321"/>
		<updated>2026-01-05T08:59:40Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update FormattingFixer: fix infobox collapsing, preserve underscores, chapter-based ref names, single button&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/(p\d+)?)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Guard to prevent duplicate WikiEditor buttons&lt;br /&gt;
    var wikiEditorButtonsAdded = false;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Generate a ref name from a chapter URL (e.g., :c37p15 or :c37_1p15 for decimals)&lt;br /&gt;
     */&lt;br /&gt;
    function generateRefName(url) {&lt;br /&gt;
        var match = config.chapterUrlPattern.exec(url);&lt;br /&gt;
        if (!match) return null;&lt;br /&gt;
        var chapter = match[1];&lt;br /&gt;
        var page = match[2];&lt;br /&gt;
        // Replace decimal with underscore for valid ref names (37.1 -&amp;gt; 37_1)&lt;br /&gt;
        var safeChapter = chapter.replace(&#039;.&#039;, &#039;_&#039;);&lt;br /&gt;
        return &#039;:c&#039; + safeChapter + &#039;p&#039; + page;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Check if it&#039;s a chapter URL&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // For other site URLs, still use Cite chapter (adjust if you have other templates)&lt;br /&gt;
        if (config.siteUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // For external URLs, could use Cite web&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Assign chapter-based names to anonymous refs with chapter URLs&lt;br /&gt;
     */&lt;br /&gt;
    function assignNamesToAnonymousRefs(wikitext, refs) {&lt;br /&gt;
        var usedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
        &lt;br /&gt;
        // Sort by index descending to preserve positions when replacing&lt;br /&gt;
        var anonsToName = refs.anonymous.filter(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            return url &amp;amp;&amp;amp; config.chapterUrlPattern.test(url);&lt;br /&gt;
        }).sort(function(a, b) { return b.index - a.index; });&lt;br /&gt;
        &lt;br /&gt;
        anonsToName.forEach(function(anon) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var baseName = generateRefName(url);&lt;br /&gt;
            if (!baseName) return;&lt;br /&gt;
            &lt;br /&gt;
            // Ensure unique name&lt;br /&gt;
            var name = baseName;&lt;br /&gt;
            var suffix = 2;&lt;br /&gt;
            while (usedNames.has(name)) {&lt;br /&gt;
                name = baseName + &#039;_&#039; + suffix;&lt;br /&gt;
                suffix++;&lt;br /&gt;
            }&lt;br /&gt;
            usedNames.add(name);&lt;br /&gt;
            &lt;br /&gt;
            // Replace anonymous ref with named ref&lt;br /&gt;
            var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            wikitext = wikitext.substring(0, anon.index) + namedRef + wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
            fixes.push(&#039;Named anonymous ref as &amp;quot;&#039; + name + &#039;&amp;quot;&#039;);&lt;br /&gt;
        });&lt;br /&gt;
        &lt;br /&gt;
        return { wikitext: wikitext, fixes: fixes };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Check if it&#039;s a raw URL to our site&lt;br /&gt;
            if (config.siteUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. Assign names to anonymous refs with chapter URLs&lt;br /&gt;
        var namingResult = assignNamesToAnonymousRefs(wikitext, refs);&lt;br /&gt;
        if (namingResult.fixes.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = namingResult.wikitext;&lt;br /&gt;
            fixes = fixes.concat(namingResult.fixes);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 6. Re-check undefined refs after naming&lt;br /&gt;
        undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Undefined references requiring manual attention ===&#039;);&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + undef.usages.length + &#039; time(s) but never defined.&#039;);&lt;br /&gt;
                warnings.push(&#039;  → Find the correct source and add: &amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=...}}&amp;lt;/ref&amp;gt;&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function veIsAvailable() {&lt;br /&gt;
        return typeof ve !== &#039;undefined&#039; &amp;amp;&amp;amp; ve.init &amp;amp;&amp;amp; ve.init.target;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetSurface() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            return null;&lt;br /&gt;
        }&lt;br /&gt;
        return ve.init.target.getSurface &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetMode() {&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        return surface &amp;amp;&amp;amp; surface.getMode ? surface.getMode() : null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veGetFullWikitextFromSourceSurface( surface ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        var doc = model.getDocument();&lt;br /&gt;
        var range = new ve.Range( 0, doc.data.getLength() );&lt;br /&gt;
        return doc.data.getSourceText( range );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllWikitextInSourceSurface( surface, newWikitext ) {&lt;br /&gt;
        var model = surface.getModel();&lt;br /&gt;
        model.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertContent( newWikitext );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veCreateDmDocumentFromWikitext( wikitext, targetDoc ) {&lt;br /&gt;
        return ve.init.target.parseWikitextFragment( wikitext, false, targetDoc ).then( function ( response ) {&lt;br /&gt;
            if ( ve.getProp( response, &#039;visualeditor&#039;, &#039;result&#039; ) !== &#039;success&#039; ) {&lt;br /&gt;
                return $.Deferred().reject( response ).promise();&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var html = response.visualeditor.content;&lt;br /&gt;
            var htmlDoc = ve.createDocumentFromHtml( html );&lt;br /&gt;
&lt;br /&gt;
            // Mirror VE&#039;s own clipboard importer flow for Parsoid HTML&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripRestbaseIds ) {&lt;br /&gt;
                mw.libs.ve.stripRestbaseIds( htmlDoc );&lt;br /&gt;
            }&lt;br /&gt;
            if ( mw.libs &amp;amp;&amp;amp; mw.libs.ve &amp;amp;&amp;amp; mw.libs.ve.stripParsoidFallbackIds ) {&lt;br /&gt;
                mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Pass an empty object for importRules to enable clipboard mode&lt;br /&gt;
            var newDoc = targetDoc.newFromHtml( htmlDoc, {} );&lt;br /&gt;
            var data = newDoc.data.data;&lt;br /&gt;
            var surface = new ve.dm.Surface( newDoc );&lt;br /&gt;
&lt;br /&gt;
            // Filter out auto-generated items (e.g. reference lists)&lt;br /&gt;
            for ( var i = data.length - 1; i &amp;gt;= 0; i-- ) {&lt;br /&gt;
                if ( ve.getProp( data[ i ], &#039;attributes&#039;, &#039;mw&#039;, &#039;autoGenerated&#039; ) ) {&lt;br /&gt;
                    surface.change(&lt;br /&gt;
                        ve.dm.TransactionBuilder.static.newFromRemoval(&lt;br /&gt;
                            newDoc,&lt;br /&gt;
                            surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()&lt;br /&gt;
                        )&lt;br /&gt;
                    );&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Avoid about attribute conflicts&lt;br /&gt;
            newDoc.data.cloneElements( true );&lt;br /&gt;
            return newDoc;&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veReplaceAllContentWithDocument( uiSurface, newDoc ) {&lt;br /&gt;
        var surfaceModel = uiSurface.getModel();&lt;br /&gt;
        surfaceModel.getLinearFragment( new ve.Range( 0 ), true )&lt;br /&gt;
            .expandLinearSelection( &#039;root&#039; )&lt;br /&gt;
            .insertDocument( newDoc );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function veEnsureSourceMode() {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( surface &amp;amp;&amp;amp; surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039; ) {&lt;br /&gt;
            return $.Deferred().resolve().promise();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Switch to the VisualEditor wikitext mode. This is the only reliable&lt;br /&gt;
        // way to apply whole-document wikitext transforms.&lt;br /&gt;
        try {&lt;br /&gt;
            return target.switchToWikitextEditor( true );&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return $.Deferred().reject( e ).promise();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function runFormattingFixerInVE() {&lt;br /&gt;
        if ( !veIsAvailable() ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: VisualEditor is not available yet.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = veGetSurface();&lt;br /&gt;
        if ( !surface ) {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not access the editor surface.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var currentMode = veGetMode();&lt;br /&gt;
&lt;br /&gt;
        // In source mode, we can read/write directly.&lt;br /&gt;
        if ( currentMode === &#039;source&#039; ) {&lt;br /&gt;
            var sourceWikitext = veGetFullWikitextFromSourceSurface( surface );&lt;br /&gt;
            var result = fixCitations( sourceWikitext );&lt;br /&gt;
            if ( result.wikitext !== sourceWikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( surface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // In visual mode, switch to source mode first to avoid Parsoid round-trip&lt;br /&gt;
        // which would collapse multiline templates and remove underscores from filenames&lt;br /&gt;
        mw.notify( &#039;FormattingFixer: Switching to source mode to preserve formatting...&#039;, { tag: &#039;formattingfixer&#039; } );&lt;br /&gt;
        veEnsureSourceMode().then( function () {&lt;br /&gt;
            var newSurface = veGetSurface();&lt;br /&gt;
            if ( !newSurface || veGetMode() !== &#039;source&#039; ) {&lt;br /&gt;
                mw.notify( &#039;FormattingFixer: Could not switch to source mode.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var wikitext = veGetFullWikitextFromSourceSurface( newSurface );&lt;br /&gt;
            var result = fixCitations( wikitext );&lt;br /&gt;
            if ( result.wikitext !== wikitext ) {&lt;br /&gt;
                veReplaceAllWikitextInSourceSurface( newSurface, result.wikitext );&lt;br /&gt;
            }&lt;br /&gt;
            showResultMessage( result );&lt;br /&gt;
        }, function () {&lt;br /&gt;
            mw.notify( &#039;FormattingFixer: Could not switch to source mode. Please switch manually and try again.&#039;, { type: &#039;error&#039; } );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        function registerTools() {&lt;br /&gt;
            // Define and register tools. Use the documented pattern:&lt;br /&gt;
            // register tools via ve.ui.toolFactory, and add a toolbar group&lt;br /&gt;
            // via target.static.toolbarGroups (early) or toolbar.addItems (late).&lt;br /&gt;
&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
&lt;br /&gt;
            if ( !toolFactory.lookup( &#039;formattingFixerFix&#039; ) ) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply( this, arguments );&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass( FormattingFixerFixTool, OO.ui.Tool );&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;formattingfixer&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToGroup = true;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function () {&lt;br /&gt;
                    runFormattingFixerInVE();&lt;br /&gt;
                    this.setActive( false );&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function () {&lt;br /&gt;
                    this.setDisabled( false );&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register( FormattingFixerFixTool );&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Ensure our tool group is available in the toolbar (early-load path).&lt;br /&gt;
        function addGroupToTarget( targetClass ) {&lt;br /&gt;
            if ( !targetClass.static.toolbarGroups ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            var exists = targetClass.static.toolbarGroups.some( function ( g ) {&lt;br /&gt;
                return g &amp;amp;&amp;amp; g.name === &#039;formattingfixer&#039;;&lt;br /&gt;
            } );&lt;br /&gt;
            if ( exists ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            targetClass.static.toolbarGroups.push( {&lt;br /&gt;
                name: &#039;formattingfixer&#039;,&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                type: &#039;list&#039;,&lt;br /&gt;
                indicator: &#039;down&#039;,&lt;br /&gt;
                include: [ { group: &#039;formattingfixer&#039; } ]&lt;br /&gt;
            } );&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Preferred: integrate during VE module loading.&lt;br /&gt;
        mw.hook( &#039;ve.loadModules&#039; ).add( function ( addPlugin ) {&lt;br /&gt;
            addPlugin( function () {&lt;br /&gt;
                registerTools();&lt;br /&gt;
                mw.loader.using( [ &#039;ext.visualEditor.mediawiki&#039; ] ).then( function () {&lt;br /&gt;
                    for ( var n in ve.init.mw.targetFactory.registry ) {&lt;br /&gt;
                        addGroupToTarget( ve.init.mw.targetFactory.lookup( n ) );&lt;br /&gt;
                    }&lt;br /&gt;
                    ve.init.mw.targetFactory.on( &#039;register&#039;, function ( name, targetClass ) {&lt;br /&gt;
                        addGroupToTarget( targetClass );&lt;br /&gt;
                    } );&lt;br /&gt;
                } );&lt;br /&gt;
            } );&lt;br /&gt;
        } );&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if VE is already active, add a toolgroup to the existing toolbar.&lt;br /&gt;
        mw.hook( &#039;ve.activationComplete&#039; ).add( function () {&lt;br /&gt;
            if ( !veIsAvailable() ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            registerTools();&lt;br /&gt;
&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar &amp;amp;&amp;amp; ve.init.target.getToolbar();&lt;br /&gt;
            if ( !toolbar ) {&lt;br /&gt;
                toolbar = ve.init.target.toolbar;&lt;br /&gt;
            }&lt;br /&gt;
            if ( !toolbar || toolbar.$element.find( &#039;.formattingfixer-toolgroup&#039; ).length ) {&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var myToolGroup = new OO.ui.ListToolGroup( toolbar, {&lt;br /&gt;
                label: &#039;FormattingFixer&#039;,&lt;br /&gt;
                include: [ &#039;formattingFixerFix&#039; ]&lt;br /&gt;
            } );&lt;br /&gt;
            myToolGroup.$element.addClass( &#039;formattingfixer-toolgroup&#039; );&lt;br /&gt;
            toolbar.addItems( [ myToolGroup ] );&lt;br /&gt;
        } );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        // Guard against duplicate button creation&lt;br /&gt;
        if (wikiEditorButtonsAdded) {&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if button already exists in DOM&lt;br /&gt;
        if ($(&#039;.tool[rel=&amp;quot;formattingfixer-fix&amp;quot;]&#039;).length &amp;gt; 0) {&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            return true;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        oouiIcon: &#039;check&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            wikiEditorButtonsAdded = true;&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor button added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Don&#039;t attach to VisualEditor&#039;s hidden dummy textbox&lt;br /&gt;
        if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // If this is the classic source editor, try loading WikiEditor explicitly.&lt;br /&gt;
            // (If the user doesn&#039;t have it enabled, we&#039;ll fall back to simple buttons.)&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (!textbox) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
                if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                    return;&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                mw.loader.using([&#039;ext.wikiEditor&#039;]).then(function() {&lt;br /&gt;
                    addWikiEditorButtons();&lt;br /&gt;
                }, function() {&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                });&lt;br /&gt;
            }, 0);&lt;br /&gt;
&lt;br /&gt;
            // If WikiEditor isn&#039;t present, ensure the user still gets controls&lt;br /&gt;
            // in the classic source editor.&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    if (typeof $ !== &#039;undefined&#039; &amp;amp;&amp;amp; $.fn &amp;amp;&amp;amp; $.fn.wikiEditor) {&lt;br /&gt;
                        // WikiEditor exists but may not be initialized yet.&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 250);&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    if (textbox.classList &amp;amp;&amp;amp; textbox.classList.contains(&#039;ve-dummyTextbox&#039;)) {&lt;br /&gt;
                        return;&lt;br /&gt;
                    }&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        generateRefName: generateRefName&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3320</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3320"/>
		<updated>2026-01-05T08:38:35Z</updated>

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

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

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

		<summary type="html">&lt;p&gt;Maintenance script: Fix FormattingFixer gadget filename (avoid Gadget-Gadget double prefix)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* FormattingFixer[ResourceLoader|default]|FormattingFixer.js&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadgets-definition&amp;diff=3316</id>
		<title>MediaWiki:Gadgets-definition</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadgets-definition&amp;diff=3316"/>
		<updated>2026-01-05T07:11:30Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update FormattingFixer gadget definition - remove WikiEditor dependency&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3315</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3315"/>
		<updated>2026-01-05T07:11:22Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Update FormattingFixer with VisualEditor support&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with:&lt;br /&gt;
 * - VisualEditor (both visual and source modes)&lt;br /&gt;
 * - WikiEditor (legacy source editor)&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/p\d+)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Check if it&#039;s a chapter URL&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // For other site URLs, still use Cite chapter (adjust if you have other templates)&lt;br /&gt;
        if (config.siteUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // For external URLs, could use Cite web&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Check if it&#039;s a raw URL to our site&lt;br /&gt;
            if (config.siteUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. If there are undefined refs, try to help match them&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0 &amp;amp;&amp;amp; refs.anonymous.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Potential matches for undefined refs ===&#039;);&lt;br /&gt;
            warnings.push(&#039;Anonymous refs that could be assigned names:&#039;);&lt;br /&gt;
            refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                warnings.push(&#039;  Anonymous #&#039; + (i+1) + &#039;: &#039; + (url || anon.content.substring(0, 50)));&lt;br /&gt;
            });&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;To fix: Add name=&amp;quot;X&amp;quot; to the anonymous ref that should define each name.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Interactive undefined ref fixer&lt;br /&gt;
     */&lt;br /&gt;
    function interactiveFixUndefined(wikitext) {&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        &lt;br /&gt;
        if (undefinedRefs.length === 0) {&lt;br /&gt;
            return { wikitext: wikitext, message: &#039;No undefined references found.&#039; };&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // For each undefined ref, prompt user to select an anonymous ref&lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            var options = [&#039;[Skip - leave undefined]&#039;, &#039;[Create placeholder]&#039;];&lt;br /&gt;
            refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                options.push(&#039;Anonymous #&#039; + (i+1) + &#039;: &#039; + (url || anon.content.substring(0, 60)));&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            var message = &#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used but never defined.\n\n&#039; +&lt;br /&gt;
                         &#039;Select which anonymous ref should define it:\n\n&#039; +&lt;br /&gt;
                         options.map(function(o, i) { return i + &#039;: &#039; + o; }).join(&#039;\n&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var choice = prompt(message, &#039;0&#039;);&lt;br /&gt;
            if (choice === null) return; // Cancelled&lt;br /&gt;
            &lt;br /&gt;
            var choiceNum = parseInt(choice, 10);&lt;br /&gt;
            &lt;br /&gt;
            if (choiceNum === 1) {&lt;br /&gt;
                // Create placeholder - find first usage and replace with definition&lt;br /&gt;
                var firstUsage = undef.usages[0];&lt;br /&gt;
                var placeholder = &#039;&amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=UNKNOWN_URL_PLEASE_FIX}}&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
                wikitext = wikitext.substring(0, firstUsage.index) + &lt;br /&gt;
                           placeholder + &lt;br /&gt;
                           wikitext.substring(firstUsage.index + firstUsage.fullMatch.length);&lt;br /&gt;
            } else if (choiceNum &amp;gt;= 2) {&lt;br /&gt;
                // Assign name to selected anonymous ref&lt;br /&gt;
                var anonIndex = choiceNum - 2;&lt;br /&gt;
                var anon = refs.anonymous[anonIndex];&lt;br /&gt;
                if (anon) {&lt;br /&gt;
                    var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
                    wikitext = wikitext.substring(0, anon.index) + &lt;br /&gt;
                               namedRef + &lt;br /&gt;
                               wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
                    // Re-parse since we modified the text&lt;br /&gt;
                    refs = parseRefs(wikitext);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, message: &#039;Interactive fix complete.&#039; };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Show result message using mw.notify (nicer than alert)&lt;br /&gt;
     */&lt;br /&gt;
    function showResultMessage(result) {&lt;br /&gt;
        var message = &#039;&#039;;&lt;br /&gt;
        if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
            message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
        }&lt;br /&gt;
        if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
            message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Use mw.notify for a nicer notification, fall back to alert&lt;br /&gt;
        if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
            mw.notify(message.replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;), { &lt;br /&gt;
                title: &#039;FormattingFixer&#039;, &lt;br /&gt;
                autoHide: false,&lt;br /&gt;
                tag: &#039;formattingfixer&#039;&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            alert(message);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // VisualEditor Integration&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar buttons to VisualEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addVisualEditorButtons() {&lt;br /&gt;
        // Wait for VisualEditor to be available&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            var surface = ve.init.target.getSurface();&lt;br /&gt;
            if (!surface) return;&lt;br /&gt;
&lt;br /&gt;
            // Get the toolbar&lt;br /&gt;
            var toolbar = ve.init.target.getToolbar();&lt;br /&gt;
            if (!toolbar) return;&lt;br /&gt;
&lt;br /&gt;
            // Check if we already added our tools&lt;br /&gt;
            if (toolbar.$element.find(&#039;.formattingfixer-tool&#039;).length &amp;gt; 0) return;&lt;br /&gt;
&lt;br /&gt;
            // Create a tool group for our buttons&lt;br /&gt;
            var toolFactory = ve.ui.toolFactory;&lt;br /&gt;
            var toolGroupFactory = ve.ui.toolGroupFactory;&lt;br /&gt;
&lt;br /&gt;
            // Define the Fix Citations tool&lt;br /&gt;
            if (!toolFactory.lookup(&#039;formattingFixerFix&#039;)) {&lt;br /&gt;
                function FormattingFixerFixTool() {&lt;br /&gt;
                    FormattingFixerFixTool.super.apply(this, arguments);&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass(FormattingFixerFixTool, ve.ui.Tool);&lt;br /&gt;
                FormattingFixerFixTool.static.name = &#039;formattingFixerFix&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.group = &#039;utility&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.title = &#039;Fix Citations&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.icon = &#039;check&#039;;&lt;br /&gt;
                FormattingFixerFixTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerFixTool.prototype.onSelect = function() {&lt;br /&gt;
                    runFormattingFixerInVE(&#039;fix&#039;);&lt;br /&gt;
                    this.setActive(false);&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerFixTool.prototype.onUpdateState = function() {&lt;br /&gt;
                    this.setDisabled(false);&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register(FormattingFixerFixTool);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Define the Fix Undefined Refs tool&lt;br /&gt;
            if (!toolFactory.lookup(&#039;formattingFixerUndefined&#039;)) {&lt;br /&gt;
                function FormattingFixerUndefinedTool() {&lt;br /&gt;
                    FormattingFixerUndefinedTool.super.apply(this, arguments);&lt;br /&gt;
                }&lt;br /&gt;
                OO.inheritClass(FormattingFixerUndefinedTool, ve.ui.Tool);&lt;br /&gt;
                FormattingFixerUndefinedTool.static.name = &#039;formattingFixerUndefined&#039;;&lt;br /&gt;
                FormattingFixerUndefinedTool.static.group = &#039;utility&#039;;&lt;br /&gt;
                FormattingFixerUndefinedTool.static.title = &#039;Fix Undefined Refs&#039;;&lt;br /&gt;
                FormattingFixerUndefinedTool.static.icon = &#039;link&#039;;&lt;br /&gt;
                FormattingFixerUndefinedTool.static.autoAddToCatchall = false;&lt;br /&gt;
                FormattingFixerUndefinedTool.prototype.onSelect = function() {&lt;br /&gt;
                    runFormattingFixerInVE(&#039;interactive&#039;);&lt;br /&gt;
                    this.setActive(false);&lt;br /&gt;
                };&lt;br /&gt;
                FormattingFixerUndefinedTool.prototype.onUpdateState = function() {&lt;br /&gt;
                    this.setDisabled(false);&lt;br /&gt;
                };&lt;br /&gt;
                toolFactory.register(FormattingFixerUndefinedTool);&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            // Add a tool group to the toolbar&lt;br /&gt;
            toolbar.setup([&lt;br /&gt;
                {&lt;br /&gt;
                    name: &#039;formattingfixer&#039;,&lt;br /&gt;
                    type: &#039;bar&#039;,&lt;br /&gt;
                    include: [&#039;formattingFixerFix&#039;, &#039;formattingFixerUndefined&#039;]&lt;br /&gt;
                }&lt;br /&gt;
            ], toolbar.getSurface());&lt;br /&gt;
&lt;br /&gt;
            // Mark as added&lt;br /&gt;
            toolbar.$element.find(&#039;.oo-ui-toolbar-tools&#039;).last().addClass(&#039;formattingfixer-tool&#039;);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Run FormattingFixer in VisualEditor context&lt;br /&gt;
     */&lt;br /&gt;
    function runFormattingFixerInVE(mode) {&lt;br /&gt;
        var target = ve.init.target;&lt;br /&gt;
        var surface = target.getSurface();&lt;br /&gt;
        &lt;br /&gt;
        if (!surface) {&lt;br /&gt;
            mw.notify(&#039;Error: Could not access editor surface&#039;, { type: &#039;error&#039; });&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Get the current wikitext - different approach for source vs visual mode&lt;br /&gt;
        var surfaceModel = surface.getModel();&lt;br /&gt;
        var doc = surfaceModel.getDocument();&lt;br /&gt;
        &lt;br /&gt;
        // Check if we&#039;re in source mode (CodeMirror/source editing)&lt;br /&gt;
        if (surface.getMode &amp;amp;&amp;amp; surface.getMode() === &#039;source&#039;) {&lt;br /&gt;
            // Source mode - get text directly&lt;br /&gt;
            var sourceText = doc.data.getText();&lt;br /&gt;
            processWikitext(sourceText, mode, function(newText) {&lt;br /&gt;
                // Replace all content&lt;br /&gt;
                var range = new ve.Range(0, doc.data.getLength());&lt;br /&gt;
                surfaceModel.change(&lt;br /&gt;
                    ve.dm.TransactionBuilder.static.newFromReplacement(doc, range, newText)&lt;br /&gt;
                );&lt;br /&gt;
            });&lt;br /&gt;
        } else {&lt;br /&gt;
            // Visual mode - need to serialize to wikitext first&lt;br /&gt;
            target.serialize(doc).then(function(wikitext) {&lt;br /&gt;
                processWikitext(wikitext, mode, function(newText) {&lt;br /&gt;
                    if (newText !== wikitext) {&lt;br /&gt;
                        // Parse the new wikitext back into the document&lt;br /&gt;
                        // This is complex - easier to switch to source mode&lt;br /&gt;
                        mw.notify(&#039;Changes made. Switching to source mode to apply...&#039;, { tag: &#039;formattingfixer&#039; });&lt;br /&gt;
                        target.switchToWikitextEditor(false, false).then(function() {&lt;br /&gt;
                            // Now in source mode, apply the changes&lt;br /&gt;
                            var newSurface = target.getSurface();&lt;br /&gt;
                            var newDoc = newSurface.getModel().getDocument();&lt;br /&gt;
                            var range = new ve.Range(0, newDoc.data.getLength());&lt;br /&gt;
                            newSurface.getModel().change(&lt;br /&gt;
                                ve.dm.TransactionBuilder.static.newFromReplacement(newDoc, range, newText)&lt;br /&gt;
                            );&lt;br /&gt;
                        });&lt;br /&gt;
                    }&lt;br /&gt;
                });&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Process wikitext and call callback with result&lt;br /&gt;
     */&lt;br /&gt;
    function processWikitext(wikitext, mode, callback) {&lt;br /&gt;
        var result;&lt;br /&gt;
        if (mode === &#039;fix&#039;) {&lt;br /&gt;
            result = fixCitations(wikitext);&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
            callback(result.wikitext);&lt;br /&gt;
        } else if (mode === &#039;interactive&#039;) {&lt;br /&gt;
            result = interactiveFixUndefined(wikitext);&lt;br /&gt;
            if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
                mw.notify(result.message, { title: &#039;FormattingFixer&#039;, tag: &#039;formattingfixer&#039; });&lt;br /&gt;
            } else {&lt;br /&gt;
                alert(result.message);&lt;br /&gt;
            }&lt;br /&gt;
            callback(result.wikitext);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // WikiEditor Integration (Legacy)&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addWikiEditorButtons() {&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor not available&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        var $textarea = $(&#039;#wpTextbox1&#039;);&lt;br /&gt;
        if ($textarea.length === 0) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: No textarea found&#039;);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        try {&lt;br /&gt;
            $textarea.wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
                section: &#039;advanced&#039;,&lt;br /&gt;
                group: &#039;format&#039;,&lt;br /&gt;
                tools: {&lt;br /&gt;
                    &#039;formattingfixer-fix&#039;: {&lt;br /&gt;
                        label: &#039;Fix Citations&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        icon: &#039;//upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-emblem-success.svg/22px-Commons-emblem-success.svg.png&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = fixCitations(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                showResultMessage(result);&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    },&lt;br /&gt;
                    &#039;formattingfixer-undefined&#039;: {&lt;br /&gt;
                        label: &#039;Fix Undefined Refs&#039;,&lt;br /&gt;
                        type: &#039;button&#039;,&lt;br /&gt;
                        icon: &#039;//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Disambig_colour.svg/22px-Disambig_colour.svg.png&#039;,&lt;br /&gt;
                        action: {&lt;br /&gt;
                            type: &#039;callback&#039;,&lt;br /&gt;
                            execute: function() {&lt;br /&gt;
                                var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                                var result = interactiveFixUndefined(textarea.value);&lt;br /&gt;
                                textarea.value = result.wikitext;&lt;br /&gt;
                                if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
                                    mw.notify(result.message, { title: &#039;FormattingFixer&#039; });&lt;br /&gt;
                                } else {&lt;br /&gt;
                                    alert(result.message);&lt;br /&gt;
                                }&lt;br /&gt;
                            }&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
            console.log(&#039;FormattingFixer: WikiEditor buttons added&#039;);&lt;br /&gt;
            return true;&lt;br /&gt;
        } catch (e) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Error adding WikiEditor buttons:&#039;, e);&lt;br /&gt;
            return false;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Fallback: Simple Buttons&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add simple HTML buttons as fallback&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (!textbox) return false;&lt;br /&gt;
&lt;br /&gt;
        // Check if buttons already exist&lt;br /&gt;
        if (document.getElementById(&#039;formattingfixer-buttons&#039;)) return true;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.id = &#039;formattingfixer-buttons&#039;;&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 8px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; display: flex; gap: 10px; align-items: center;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var label = document.createElement(&#039;span&#039;);&lt;br /&gt;
        label.textContent = &#039;FormattingFixer:&#039;;&lt;br /&gt;
        label.style.fontWeight = &#039;bold&#039;;&lt;br /&gt;
        container.appendChild(label);&lt;br /&gt;
&lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;🔧 Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var result = fixCitations(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            showResultMessage(result);&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
&lt;br /&gt;
        var interactiveBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        interactiveBtn.textContent = &#039;🔗 Fix Undefined Refs&#039;;&lt;br /&gt;
        interactiveBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer; border: 1px solid #a2a9b1; border-radius: 2px; background: #fff;&#039;;&lt;br /&gt;
        interactiveBtn.type = &#039;button&#039;;&lt;br /&gt;
        interactiveBtn.onclick = function() {&lt;br /&gt;
            var result = interactiveFixUndefined(textbox.value);&lt;br /&gt;
            textbox.value = result.wikitext;&lt;br /&gt;
            if (typeof mw !== &#039;undefined&#039; &amp;amp;&amp;amp; mw.notify) {&lt;br /&gt;
                mw.notify(result.message, { title: &#039;FormattingFixer&#039; });&lt;br /&gt;
            } else {&lt;br /&gt;
                alert(result.message);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
        container.appendChild(interactiveBtn);&lt;br /&gt;
&lt;br /&gt;
        textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        console.log(&#039;FormattingFixer: Simple buttons added&#039;);&lt;br /&gt;
        return true;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // ========================================&lt;br /&gt;
    // Initialization&lt;br /&gt;
    // ========================================&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        var veLoaded = mw.config.get(&#039;wgVisualEditor&#039;);&lt;br /&gt;
&lt;br /&gt;
        console.log(&#039;FormattingFixer: Initializing, action=&#039; + action);&lt;br /&gt;
&lt;br /&gt;
        // For VisualEditor&lt;br /&gt;
        if (typeof ve !== &#039;undefined&#039; || (veLoaded &amp;amp;&amp;amp; veLoaded.pageLanguageDir)) {&lt;br /&gt;
            console.log(&#039;FormattingFixer: Setting up VisualEditor hooks&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Hook for when VE loads later&lt;br /&gt;
        mw.hook(&#039;ve.activationComplete&#039;).add(function() {&lt;br /&gt;
            console.log(&#039;FormattingFixer: VE activation complete&#039;);&lt;br /&gt;
            addVisualEditorButtons();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // For source editing (action=edit without VE)&lt;br /&gt;
        if (action === &#039;edit&#039; || action === &#039;submit&#039;) {&lt;br /&gt;
            // Try WikiEditor first&lt;br /&gt;
            mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
                console.log(&#039;FormattingFixer: WikiEditor toolbar ready&#039;);&lt;br /&gt;
                addWikiEditorButtons();&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            // Fallback after delay&lt;br /&gt;
            setTimeout(function() {&lt;br /&gt;
                var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                if (textbox &amp;amp;&amp;amp; !document.getElementById(&#039;formattingfixer-buttons&#039;)) {&lt;br /&gt;
                    // Check if WikiEditor toolbar exists&lt;br /&gt;
                    if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                        if (!addWikiEditorButtons()) {&lt;br /&gt;
                            addSimpleButtons();&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        addSimpleButtons();&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }, 1500);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Run when ready&lt;br /&gt;
    if (document.readyState === &#039;loading&#039;) {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
            mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
        });&lt;br /&gt;
    } else {&lt;br /&gt;
        mw.loader.using([&#039;mediawiki.util&#039;]).then(init);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        interactiveFixUndefined: interactiveFixUndefined&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadgets-definition&amp;diff=3314</id>
		<title>MediaWiki:Gadgets-definition</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadgets-definition&amp;diff=3314"/>
		<updated>2026-01-05T07:02:41Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add FormattingFixer gadget definition&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* FormattingFixer[ResourceLoader|default|dependencies=ext.wikiEditor]|Gadget-FormattingFixer.js&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3313</id>
		<title>MediaWiki:Gadget-FormattingFixer.js</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=MediaWiki:Gadget-FormattingFixer.js&amp;diff=3313"/>
		<updated>2026-01-05T07:02:40Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: Add FormattingFixer gadget&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/**&lt;br /&gt;
 * FormattingFixer for MediaWiki&lt;br /&gt;
 * &lt;br /&gt;
 * Fixes common reference citation issues in wikitext:&lt;br /&gt;
 * - Reorders refs so definitions come before reuses&lt;br /&gt;
 * - Standardizes chapter URLs to {{Cite chapter|url=...}} format&lt;br /&gt;
 * - Detects and helps fix undefined named refs&lt;br /&gt;
 * - Validates chapter URL format (must have /cXX/pXX)&lt;br /&gt;
 * &lt;br /&gt;
 * Works with WikiEditor (source editing mode).&lt;br /&gt;
 * Note: Does NOT work with VisualEditor as it requires raw wikitext access.&lt;br /&gt;
 * &lt;br /&gt;
 * Installation:&lt;br /&gt;
 * 1. Copy this code to MediaWiki:Gadget-FormattingFixer.js&lt;br /&gt;
 * 2. Add to MediaWiki:Gadgets-definition:&lt;br /&gt;
 *    * FormattingFixer[ResourceLoader|default|dependencies=ext.wikiEditor]|Gadget-FormattingFixer.js&lt;br /&gt;
 * 3. All users get it by default; can disable in Special:Preferences &amp;gt; Gadgets&lt;br /&gt;
 */&lt;br /&gt;
(function() {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Configuration - adjust this pattern if your wiki uses a different URL structure&lt;br /&gt;
    var config = {&lt;br /&gt;
        chapterUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)\/p(\d+)/,&lt;br /&gt;
        chapterUrlLoose: /https?:\/\/www\.bittersweetcandybowl\.com\/c(\d+(?:\.\d+)?)(\/p\d+)?/,&lt;br /&gt;
        siteUrlPattern: /https?:\/\/www\.bittersweetcandybowl\.com\//&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Parse all references from wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function parseRefs(wikitext) {&lt;br /&gt;
        var refs = {&lt;br /&gt;
            definitions: [],  // &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
            reuses: [],       // &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
            anonymous: []     // &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // Match named refs with content: &amp;lt;ref name=&amp;quot;X&amp;quot;&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        var defined_refPattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        var match;&lt;br /&gt;
        while ((match = defined_refPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.definitions.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                content: match[2],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match named refs without content (reuses): &amp;lt;ref name=&amp;quot;X&amp;quot; /&amp;gt;&lt;br /&gt;
        var reusePattern = /&amp;lt;ref\s+name\s*=\s*[&amp;quot;&#039;]([^&amp;quot;&#039;]+)[&amp;quot;&#039;]\s*\/&amp;gt;/gi;&lt;br /&gt;
        while ((match = reusePattern.exec(wikitext)) !== null) {&lt;br /&gt;
            refs.reuses.push({&lt;br /&gt;
                fullMatch: match[0],&lt;br /&gt;
                name: match[1],&lt;br /&gt;
                index: match.index&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Match anonymous refs: &amp;lt;ref&amp;gt;content&amp;lt;/ref&amp;gt;&lt;br /&gt;
        // Need to be careful not to match named refs&lt;br /&gt;
        var anonPattern = /&amp;lt;ref\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        while ((match = anonPattern.exec(wikitext)) !== null) {&lt;br /&gt;
            // Double-check it&#039;s not a named ref that our pattern somehow caught&lt;br /&gt;
            if (!/&amp;lt;ref\s+name\s*=/.test(match[0])) {&lt;br /&gt;
                refs.anonymous.push({&lt;br /&gt;
                    fullMatch: match[0],&lt;br /&gt;
                    content: match[1],&lt;br /&gt;
                    index: match.index&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return refs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Extract URL from ref content&lt;br /&gt;
     */&lt;br /&gt;
    function extractUrl(content) {&lt;br /&gt;
        // Try {{Cite chapter|url=...}} format first&lt;br /&gt;
        var citeMatch = /\{\{Cite\s*chapter\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (citeMatch) {&lt;br /&gt;
            return citeMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Try {{Cite web|url=...}} format&lt;br /&gt;
        var webMatch = /\{\{Cite\s*web\s*\|\s*url\s*=\s*([^\s\}\|]+)/i.exec(content);&lt;br /&gt;
        if (webMatch) {&lt;br /&gt;
            return webMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Try raw URL&lt;br /&gt;
        var urlMatch = /(https?:\/\/[^\s\}&amp;lt;]+)/.exec(content);&lt;br /&gt;
        if (urlMatch) {&lt;br /&gt;
            return urlMatch[1];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return null;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Format a URL as proper citation&lt;br /&gt;
     */&lt;br /&gt;
    function formatCitation(url) {&lt;br /&gt;
        if (!url) return null;&lt;br /&gt;
        &lt;br /&gt;
        // Check if it&#039;s a chapter URL&lt;br /&gt;
        if (config.chapterUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // For other site URLs, still use Cite chapter (adjust if you have other templates)&lt;br /&gt;
        if (config.siteUrlPattern.test(url)) {&lt;br /&gt;
            return &#039;{{Cite chapter|url=&#039; + url + &#039;}}&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // For external URLs, could use Cite web&lt;br /&gt;
        return url;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Validate a chapter URL has proper format&lt;br /&gt;
     */&lt;br /&gt;
    function validateChapterUrl(url) {&lt;br /&gt;
        if (!url) return { valid: false, error: &#039;No URL found&#039; };&lt;br /&gt;
        &lt;br /&gt;
        var looseMatch = config.chapterUrlLoose.exec(url);&lt;br /&gt;
        if (!looseMatch) {&lt;br /&gt;
            return { valid: true, error: null }; // Not a chapter URL, skip validation&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // It&#039;s a chapter URL - check if it has /pXX&lt;br /&gt;
        if (!looseMatch[2]) {&lt;br /&gt;
            return { &lt;br /&gt;
                valid: false, &lt;br /&gt;
                error: &#039;Chapter URL missing page number: &#039; + url + &#039; (needs /pXX)&#039;&lt;br /&gt;
            };&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return { valid: true, error: null };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find order issues - refs used before they&#039;re defined&lt;br /&gt;
     */&lt;br /&gt;
    function findOrderIssues(refs) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var definitionsByName = {};&lt;br /&gt;
&lt;br /&gt;
        // Index definitions by name&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            if (!definitionsByName[def.name]) {&lt;br /&gt;
                definitionsByName[def.name] = def;&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Check each reuse&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            var def = definitionsByName[reuse.name];&lt;br /&gt;
            if (def &amp;amp;&amp;amp; reuse.index &amp;lt; def.index) {&lt;br /&gt;
                issues.push({&lt;br /&gt;
                    name: reuse.name,&lt;br /&gt;
                    reuseIndex: reuse.index,&lt;br /&gt;
                    definitionIndex: def.index,&lt;br /&gt;
                    definition: def,&lt;br /&gt;
                    reuse: reuse&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return issues;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find undefined refs - names used but never defined&lt;br /&gt;
     */&lt;br /&gt;
    function findUndefinedRefs(refs) {&lt;br /&gt;
        var definedNames = new Set(refs.definitions.map(function(d) { return d.name; }));&lt;br /&gt;
        var undefinedRefs = [];&lt;br /&gt;
&lt;br /&gt;
        refs.reuses.forEach(function(reuse) {&lt;br /&gt;
            if (!definedNames.has(reuse.name)) {&lt;br /&gt;
                // Check if we already logged this name&lt;br /&gt;
                if (!undefinedRefs.some(function(u) { return u.name === reuse.name; })) {&lt;br /&gt;
                    undefinedRefs.push({&lt;br /&gt;
                        name: reuse.name,&lt;br /&gt;
                        usages: refs.reuses.filter(function(r) { return r.name === reuse.name; })&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return undefinedRefs;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Find anonymous refs that could match undefined names&lt;br /&gt;
     */&lt;br /&gt;
    function findPotentialMatches(refs, undefinedRefs) {&lt;br /&gt;
        var matches = [];&lt;br /&gt;
        &lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            refs.anonymous.forEach(function(anon) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                if (url) {&lt;br /&gt;
                    matches.push({&lt;br /&gt;
                        undefinedName: undef.name,&lt;br /&gt;
                        anonymousRef: anon,&lt;br /&gt;
                        url: url&lt;br /&gt;
                    });&lt;br /&gt;
                }&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return matches;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Fix order issues in wikitext&lt;br /&gt;
     */&lt;br /&gt;
    function fixOrderIssues(wikitext, issues) {&lt;br /&gt;
        if (issues.length === 0) return wikitext;&lt;br /&gt;
&lt;br /&gt;
        // Sort issues by definition index descending (fix from end to preserve indices)&lt;br /&gt;
        issues.sort(function(a, b) { return b.definitionIndex - a.definitionIndex; });&lt;br /&gt;
&lt;br /&gt;
        issues.forEach(function(issue) {&lt;br /&gt;
            // Strategy: &lt;br /&gt;
            // 1. Replace the definition with a reuse&lt;br /&gt;
            // 2. Replace the first reuse with the definition&lt;br /&gt;
            &lt;br /&gt;
            var def = issue.definition;&lt;br /&gt;
            var reuse = issue.reuse;&lt;br /&gt;
            &lt;br /&gt;
            // Build the replacement strings&lt;br /&gt;
            var reuseStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot; /&amp;gt;&#039;;&lt;br /&gt;
            var defStr = &#039;&amp;lt;ref name=&amp;quot;&#039; + def.name + &#039;&amp;quot;&amp;gt;&#039; + def.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Replace definition with reuse (do this first since it&#039;s later in the text)&lt;br /&gt;
            wikitext = wikitext.substring(0, def.index) + &lt;br /&gt;
                       reuseStr + &lt;br /&gt;
                       wikitext.substring(def.index + def.fullMatch.length);&lt;br /&gt;
            &lt;br /&gt;
            // Now replace the reuse with definition&lt;br /&gt;
            // Need to recalculate position since we changed the text&lt;br /&gt;
            var offset = reuseStr.length - def.fullMatch.length;&lt;br /&gt;
            var newReuseIndex = reuse.index; // reuse is before def, so no offset needed&lt;br /&gt;
            &lt;br /&gt;
            wikitext = wikitext.substring(0, newReuseIndex) + &lt;br /&gt;
                       defStr + &lt;br /&gt;
                       wikitext.substring(newReuseIndex + reuse.fullMatch.length);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Standardize citation format&lt;br /&gt;
     */&lt;br /&gt;
    function standardizeCitations(wikitext) {&lt;br /&gt;
        // Find all refs with raw URLs (not in Cite templates)&lt;br /&gt;
        var refPattern = /&amp;lt;ref(\s+name\s*=\s*[&amp;quot;&#039;][^&amp;quot;&#039;]+[&amp;quot;&#039;])?\s*&amp;gt;([\s\S]*?)&amp;lt;\/ref&amp;gt;/gi;&lt;br /&gt;
        &lt;br /&gt;
        wikitext = wikitext.replace(refPattern, function(match, nameAttr, content) {&lt;br /&gt;
            nameAttr = nameAttr || &#039;&#039;;&lt;br /&gt;
            &lt;br /&gt;
            // Check if content is just a raw URL&lt;br /&gt;
            var trimmedContent = content.trim();&lt;br /&gt;
            &lt;br /&gt;
            // Skip if already in Cite template&lt;br /&gt;
            if (/\{\{Cite/i.test(trimmedContent)) {&lt;br /&gt;
                return match;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            // Check if it&#039;s a raw URL to our site&lt;br /&gt;
            if (config.siteUrlPattern.test(trimmedContent)) {&lt;br /&gt;
                var url = trimmedContent;&lt;br /&gt;
                var formatted = formatCitation(url);&lt;br /&gt;
                return &#039;&amp;lt;ref&#039; + nameAttr + &#039;&amp;gt;&#039; + formatted + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            return match;&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return wikitext;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Main fix function&lt;br /&gt;
     */&lt;br /&gt;
    function fixCitations(wikitext) {&lt;br /&gt;
        var issues = [];&lt;br /&gt;
        var warnings = [];&lt;br /&gt;
        var fixes = [];&lt;br /&gt;
&lt;br /&gt;
        // Parse all refs&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
&lt;br /&gt;
        // 1. Find and fix order issues&lt;br /&gt;
        var orderIssues = findOrderIssues(refs);&lt;br /&gt;
        if (orderIssues.length &amp;gt; 0) {&lt;br /&gt;
            wikitext = fixOrderIssues(wikitext, orderIssues);&lt;br /&gt;
            orderIssues.forEach(function(issue) {&lt;br /&gt;
                fixes.push(&#039;Fixed order: &amp;quot;&#039; + issue.name + &#039;&amp;quot; - moved definition before first usage&#039;);&lt;br /&gt;
            });&lt;br /&gt;
            // Re-parse after fixes&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 2. Standardize citation format&lt;br /&gt;
        var before = wikitext;&lt;br /&gt;
        wikitext = standardizeCitations(wikitext);&lt;br /&gt;
        if (wikitext !== before) {&lt;br /&gt;
            fixes.push(&#039;Standardized raw URLs to {{Cite chapter|url=...}} format&#039;);&lt;br /&gt;
            refs = parseRefs(wikitext);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 3. Find undefined refs&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0) {&lt;br /&gt;
            undefinedRefs.forEach(function(undef) {&lt;br /&gt;
                warnings.push(&#039;Undefined reference: &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used &#039; + &lt;br /&gt;
                             undef.usages.length + &#039; time(s) but never defined&#039;);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // 4. Validate chapter URLs&lt;br /&gt;
        refs.definitions.forEach(function(def) {&lt;br /&gt;
            var url = extractUrl(def.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in &amp;quot;&#039; + def.name + &#039;&amp;quot;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
        refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
            var url = extractUrl(anon.content);&lt;br /&gt;
            var validation = validateChapterUrl(url);&lt;br /&gt;
            if (!validation.valid) {&lt;br /&gt;
                warnings.push(&#039;Invalid URL in anonymous ref #&#039; + (i+1) + &#039;: &#039; + validation.error);&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // 5. If there are undefined refs, try to help match them&lt;br /&gt;
        if (undefinedRefs.length &amp;gt; 0 &amp;amp;&amp;amp; refs.anonymous.length &amp;gt; 0) {&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;=== Potential matches for undefined refs ===&#039;);&lt;br /&gt;
            warnings.push(&#039;Anonymous refs that could be assigned names:&#039;);&lt;br /&gt;
            refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                warnings.push(&#039;  Anonymous #&#039; + (i+1) + &#039;: &#039; + (url || anon.content.substring(0, 50)));&lt;br /&gt;
            });&lt;br /&gt;
            warnings.push(&#039;&#039;);&lt;br /&gt;
            warnings.push(&#039;To fix: Add name=&amp;quot;X&amp;quot; to the anonymous ref that should define each name.&#039;);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            wikitext: wikitext,&lt;br /&gt;
            fixes: fixes,&lt;br /&gt;
            warnings: warnings&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Interactive undefined ref fixer&lt;br /&gt;
     */&lt;br /&gt;
    function interactiveFixUndefined(wikitext) {&lt;br /&gt;
        var refs = parseRefs(wikitext);&lt;br /&gt;
        var undefinedRefs = findUndefinedRefs(refs);&lt;br /&gt;
        &lt;br /&gt;
        if (undefinedRefs.length === 0) {&lt;br /&gt;
            return { wikitext: wikitext, message: &#039;No undefined references found.&#039; };&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // For each undefined ref, prompt user to select an anonymous ref&lt;br /&gt;
        undefinedRefs.forEach(function(undef) {&lt;br /&gt;
            var options = [&#039;[Skip - leave undefined]&#039;, &#039;[Create placeholder]&#039;];&lt;br /&gt;
            refs.anonymous.forEach(function(anon, i) {&lt;br /&gt;
                var url = extractUrl(anon.content);&lt;br /&gt;
                options.push(&#039;Anonymous #&#039; + (i+1) + &#039;: &#039; + (url || anon.content.substring(0, 60)));&lt;br /&gt;
            });&lt;br /&gt;
&lt;br /&gt;
            var message = &#039;Reference &amp;quot;&#039; + undef.name + &#039;&amp;quot; is used but never defined.\n\n&#039; +&lt;br /&gt;
                         &#039;Select which anonymous ref should define it:\n\n&#039; +&lt;br /&gt;
                         options.map(function(o, i) { return i + &#039;: &#039; + o; }).join(&#039;\n&#039;);&lt;br /&gt;
            &lt;br /&gt;
            var choice = prompt(message, &#039;0&#039;);&lt;br /&gt;
            if (choice === null) return; // Cancelled&lt;br /&gt;
            &lt;br /&gt;
            var choiceNum = parseInt(choice, 10);&lt;br /&gt;
            &lt;br /&gt;
            if (choiceNum === 1) {&lt;br /&gt;
                // Create placeholder - find first usage and replace with definition&lt;br /&gt;
                var firstUsage = undef.usages[0];&lt;br /&gt;
                var placeholder = &#039;&amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;{{Cite chapter|url=UNKNOWN_URL_PLEASE_FIX}}&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
                wikitext = wikitext.substring(0, firstUsage.index) + &lt;br /&gt;
                           placeholder + &lt;br /&gt;
                           wikitext.substring(firstUsage.index + firstUsage.fullMatch.length);&lt;br /&gt;
            } else if (choiceNum &amp;gt;= 2) {&lt;br /&gt;
                // Assign name to selected anonymous ref&lt;br /&gt;
                var anonIndex = choiceNum - 2;&lt;br /&gt;
                var anon = refs.anonymous[anonIndex];&lt;br /&gt;
                if (anon) {&lt;br /&gt;
                    var namedRef = &#039;&amp;lt;ref name=&amp;quot;&#039; + undef.name + &#039;&amp;quot;&amp;gt;&#039; + anon.content + &#039;&amp;lt;/ref&amp;gt;&#039;;&lt;br /&gt;
                    wikitext = wikitext.substring(0, anon.index) + &lt;br /&gt;
                               namedRef + &lt;br /&gt;
                               wikitext.substring(anon.index + anon.fullMatch.length);&lt;br /&gt;
                    // Re-parse since we modified the text&lt;br /&gt;
                    refs = parseRefs(wikitext);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        return { wikitext: wikitext, message: &#039;Interactive fix complete.&#039; };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Add toolbar button for WikiEditor&lt;br /&gt;
     */&lt;br /&gt;
    function addToolbarButton() {&lt;br /&gt;
        if (typeof $ === &#039;undefined&#039; || !$.fn.wikiEditor) {&lt;br /&gt;
            console.log(&#039;WikiEditor not available&#039;);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $(&#039;#wpTextbox1&#039;).wikiEditor(&#039;addToToolbar&#039;, {&lt;br /&gt;
            section: &#039;advanced&#039;,&lt;br /&gt;
            group: &#039;format&#039;,&lt;br /&gt;
            tools: {&lt;br /&gt;
                &#039;fix-citations&#039;: {&lt;br /&gt;
                    label: &#039;Fix Citations&#039;,&lt;br /&gt;
                    type: &#039;button&#039;,&lt;br /&gt;
                    icon: &#039;//upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-emblem-success.svg/22px-Commons-emblem-success.svg.png&#039;,&lt;br /&gt;
                    action: {&lt;br /&gt;
                        type: &#039;callback&#039;,&lt;br /&gt;
                        execute: function(context) {&lt;br /&gt;
                            var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                            var result = fixCitations(textarea.value);&lt;br /&gt;
                            &lt;br /&gt;
                            var message = &#039;&#039;;&lt;br /&gt;
                            if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
                                message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
                            }&lt;br /&gt;
                            if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
                                message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
                            }&lt;br /&gt;
                            if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
                                message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
                            }&lt;br /&gt;
                            &lt;br /&gt;
                            textarea.value = result.wikitext;&lt;br /&gt;
                            alert(message);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                },&lt;br /&gt;
                &#039;fix-citations-interactive&#039;: {&lt;br /&gt;
                    label: &#039;Fix Undefined Refs&#039;,&lt;br /&gt;
                    type: &#039;button&#039;,&lt;br /&gt;
                    icon: &#039;//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Disambig_colour.svg/22px-Disambig_colour.svg.png&#039;,&lt;br /&gt;
                    action: {&lt;br /&gt;
                        type: &#039;callback&#039;,&lt;br /&gt;
                        execute: function(context) {&lt;br /&gt;
                            var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
                            var result = interactiveFixUndefined(textarea.value);&lt;br /&gt;
                            textarea.value = result.wikitext;&lt;br /&gt;
                            alert(result.message);&lt;br /&gt;
                        }&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Alternative: Add to page as simple buttons (for wikis without WikiEditor)&lt;br /&gt;
     */&lt;br /&gt;
    function addSimpleButtons() {&lt;br /&gt;
        var toolbar = document.getElementById(&#039;toolbar&#039;) || &lt;br /&gt;
                      document.querySelector(&#039;.wikiEditor-ui-toolbar&#039;) ||&lt;br /&gt;
                      document.querySelector(&#039;#wpTextbox1&#039;)?.parentNode;&lt;br /&gt;
        &lt;br /&gt;
        if (!toolbar) return;&lt;br /&gt;
&lt;br /&gt;
        var container = document.createElement(&#039;div&#039;);&lt;br /&gt;
        container.style.cssText = &#039;margin: 5px 0; padding: 5px; background: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px;&#039;;&lt;br /&gt;
        &lt;br /&gt;
        var fixBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        fixBtn.textContent = &#039;🔧 Fix Citations&#039;;&lt;br /&gt;
        fixBtn.style.cssText = &#039;margin-right: 10px; padding: 5px 10px; cursor: pointer;&#039;;&lt;br /&gt;
        fixBtn.type = &#039;button&#039;;&lt;br /&gt;
        fixBtn.onclick = function() {&lt;br /&gt;
            var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
            var result = fixCitations(textarea.value);&lt;br /&gt;
            &lt;br /&gt;
            var message = &#039;&#039;;&lt;br /&gt;
            if (result.fixes.length &amp;gt; 0) {&lt;br /&gt;
                message += &#039;FIXES APPLIED:\n&#039; + result.fixes.join(&#039;\n&#039;) + &#039;\n\n&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            if (result.warnings.length &amp;gt; 0) {&lt;br /&gt;
                message += &#039;WARNINGS:\n&#039; + result.warnings.join(&#039;\n&#039;);&lt;br /&gt;
            }&lt;br /&gt;
            if (result.fixes.length === 0 &amp;amp;&amp;amp; result.warnings.length === 0) {&lt;br /&gt;
                message = &#039;No issues found! Citations look good.&#039;;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            textarea.value = result.wikitext;&lt;br /&gt;
            alert(message);&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        var interactiveBtn = document.createElement(&#039;button&#039;);&lt;br /&gt;
        interactiveBtn.textContent = &#039;🔗 Fix Undefined Refs&#039;;&lt;br /&gt;
        interactiveBtn.style.cssText = &#039;padding: 5px 10px; cursor: pointer;&#039;;&lt;br /&gt;
        interactiveBtn.type = &#039;button&#039;;&lt;br /&gt;
        interactiveBtn.onclick = function() {&lt;br /&gt;
            var textarea = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
            var result = interactiveFixUndefined(textarea.value);&lt;br /&gt;
            textarea.value = result.wikitext;&lt;br /&gt;
            alert(result.message);&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        container.appendChild(fixBtn);&lt;br /&gt;
        container.appendChild(interactiveBtn);&lt;br /&gt;
        &lt;br /&gt;
        var textbox = document.getElementById(&#039;wpTextbox1&#039;);&lt;br /&gt;
        if (textbox) {&lt;br /&gt;
            textbox.parentNode.insertBefore(container, textbox);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Initialize - hook into WikiEditor when ready&lt;br /&gt;
     */&lt;br /&gt;
    function init() {&lt;br /&gt;
        // Only run on edit pages (source editor, not VisualEditor)&lt;br /&gt;
        var action = mw.config.get(&#039;wgAction&#039;);&lt;br /&gt;
        if (action !== &#039;edit&#039; &amp;amp;&amp;amp; action !== &#039;submit&#039;) {&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Wait for WikiEditor to be ready, then add toolbar buttons&lt;br /&gt;
        mw.hook(&#039;wikiEditor.toolbarReady&#039;).add(function($textarea) {&lt;br /&gt;
            addToolbarButton();&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        // Fallback: if WikiEditor hook doesn&#039;t fire, try after a delay&lt;br /&gt;
        setTimeout(function() {&lt;br /&gt;
            if (!window._formattingFixerInitialized) {&lt;br /&gt;
                // Check if WikiEditor toolbar exists&lt;br /&gt;
                if ($(&#039;.wikiEditor-ui-toolbar&#039;).length &amp;gt; 0) {&lt;br /&gt;
                    addToolbarButton();&lt;br /&gt;
                } else {&lt;br /&gt;
                    // No WikiEditor, add simple buttons&lt;br /&gt;
                    addSimpleButtons();&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }, 2000);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Mark as initialized when toolbar button is added&lt;br /&gt;
    var originalAddToolbarButton = addToolbarButton;&lt;br /&gt;
    addToolbarButton = function() {&lt;br /&gt;
        window._formattingFixerInitialized = true;&lt;br /&gt;
        originalAddToolbarButton();&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    // Run init when DOM and MediaWiki are ready&lt;br /&gt;
    $(function() {&lt;br /&gt;
        init();&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Expose for testing&lt;br /&gt;
    window.FormattingFixer = {&lt;br /&gt;
        fixCitations: fixCitations,&lt;br /&gt;
        parseRefs: parseRefs,&lt;br /&gt;
        interactiveFixUndefined: interactiveFixUndefined&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=File:Character_zachary.png&amp;diff=1160</id>
		<title>File:Character zachary.png</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=File:Character_zachary.png&amp;diff=1160"/>
		<updated>2024-11-08T04:42:34Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: == Summary ==
Importing file&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Importing file&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=File:Character_yashy.png&amp;diff=1159</id>
		<title>File:Character yashy.png</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=File:Character_yashy.png&amp;diff=1159"/>
		<updated>2024-11-08T04:42:34Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: == Summary ==
Importing file&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Importing file&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=File:Character_turtle.png&amp;diff=1158</id>
		<title>File:Character turtle.png</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=File:Character_turtle.png&amp;diff=1158"/>
		<updated>2024-11-08T04:42:34Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: == Summary ==
Importing file&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Importing file&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=File:Character_toby.png&amp;diff=1157</id>
		<title>File:Character toby.png</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=File:Character_toby.png&amp;diff=1157"/>
		<updated>2024-11-08T04:42:34Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: == Summary ==
Importing file&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Importing file&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
	<entry>
		<id>https://candypedia.wiki/index.php?title=File:Character_tiff.png&amp;diff=1156</id>
		<title>File:Character tiff.png</title>
		<link rel="alternate" type="text/html" href="https://candypedia.wiki/index.php?title=File:Character_tiff.png&amp;diff=1156"/>
		<updated>2024-11-08T04:42:34Z</updated>

		<summary type="html">&lt;p&gt;Maintenance script: == Summary ==
Importing file&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Importing file&lt;/div&gt;</summary>
		<author><name>Maintenance script</name></author>
	</entry>
</feed>