MediaWiki:Common.js: Difference between revisions

no edit summary
No edit summary
No edit summary
 
Line 781: Line 781:
});
});


// ==UserScript==
// @name        MediaWiki Smart Single Quotes
// @namespace    http://your.local/
// @version      1.0
// @description  Convert straight single quotes to typographic single quotes on MediaWiki pages (visual only).
// @match        https://wiki.ekatrafoundation.org/*
// @grant        none
// ==/UserScript==


(function (window, document) {
(function () {
     'use strict';
     'use strict';


    // Small API object for testing/rerun
     function convertSingleQuotesText(text) {
    window.smartQuotes = window.smartQuotes || {};
 
    // Convert single quotes in a text string to smart quotes
     function convertSingleQuotes(text) {
         if (!text || text.indexOf("'") === -1) return text;
         if (!text || text.indexOf("'") === -1) return text;
        // Protect contractions/possessives like don't or John's
         text = text.replace(/([A-Za-z0-9])'([A-Za-z0-9])/g, "$1’$2"); // contractions/possessives
         text = text.replace(/([A-Za-z0-9])'([A-Za-z0-9])/g, "$1’$2");
         text = text.replace(/(^|[\s\(\[\{\<\u2014\u2013"“'«])'(?=\S)/g, "$1‘"); // opening quotes
        // Opening single quote when at start or after whitespace/open punctuation
         text = text.replace(/'/g, "’"); // remaining closing quotes
         text = text.replace(/(^|[\s\(\[\{\<\u2014\u2013"“'«])'(?=\S)/g, "$1‘");
        // Remaining single quotes are closing
         text = text.replace(/'/g, "’");
         return text;
         return text;
     }
     }


    // Walk text nodes under root and replace text content (skip code-like tags)
     function walkAndReplace(root) {
     function walkAndReplace(root) {
         if (!root) return;
         if (!root) return;
         try {
         try {
             var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
             var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
             var node;
             var n;
             var blacklist = new Set(["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA", "NOSCRIPT", "MATH", "INPUT", "TEXTAREA"]);
             var blacklist = new Set(["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA", "NOSCRIPT", "MATH", "INPUT"]);
             while ((node = walker.nextNode())) {
             while ((n = walker.nextNode())) {
                 var parent = node.parentNode;
                 var parent = n.parentNode;
                 if (!parent) continue;
                 if (!parent) continue;
                // Skip nodes with blacklisted ancestor
                 var anc = parent, skip = false;
                 var anc = parent, skip = false;
                 while (anc && anc.nodeType === 1) {
                 while (anc && anc.nodeType === 1) {
Line 817: Line 816:
                 }
                 }
                 if (skip) continue;
                 if (skip) continue;
                 var original = node.nodeValue;
                 var orig = n.nodeValue;
                 var replaced = convertSingleQuotes(original);
                 var rep = convertSingleQuotesText(orig);
                 if (replaced !== original) node.nodeValue = replaced;
                 if (rep !== orig) n.nodeValue = rep;
             }
             }
         } catch (e) {
         } catch (e) {
             // Defensive: log but don't throw
             console.error('smartQuotes error', e);
            if (window.console && window.console.error) {
                console.error('smartQuotes.walkAndReplace error:', e);
            }
         }
         }
     }
     }


    // Process main content area (parser output) then body fallback
     function applyOnce() {
     function processOnce() {
         var root = document.querySelector('.mw-parser-output') || document.body;
         var root = document.querySelector('.mw-parser-output') || document.body;
         walkAndReplace(root);
         walkAndReplace(root);
     }
     }


     // Observe for dynamic/AJAX content
     // run after window.load, also watch for AJAX content
     function initObserver() {
     window.addEventListener('load', function () {
         try {
         setTimeout(applyOnce, 150);
            var obs = new MutationObserver(function (mutations) {
        var obs = new MutationObserver(function (mutations) {
                mutations.forEach(function (m) {
            mutations.forEach(function (m) {
                    m.addedNodes && m.addedNodes.forEach(function (n) {
                m.addedNodes && m.addedNodes.forEach(function (n) {
                        if (n.nodeType === 1) walkAndReplace(n);
                    if (n.nodeType === 1) walkAndReplace(n);
                        else if (n.nodeType === 3 && n.parentNode) walkAndReplace(n.parentNode);
                    else if (n.nodeType === 3 && n.parentNode) walkAndReplace(n.parentNode);
                    });
                 });
                 });
             });
             });
            obs.observe(document.body, { childList: true, subtree: true });
        });
            window.smartQuotes._observer = obs;
        obs.observe(document.body, { childList: true, subtree: true });
        } catch (e) {
     }, false);
            console.error('smartQuotes.initObserver error:', e);
})();
        }
    }
 
    // Public API to run now (useful to trigger from console)
    window.smartQuotes.runNow = function () {
        processOnce();
        if (!window.smartQuotes._observer) initObserver();
        return true;
     };
 
    // Run after full window load — this avoids ResourceLoader bundle errors
    function runAfterLoad() {
        try {
            // Use requestIdleCallback if available for low priority, fallback to setTimeout
            var run = function () { window.smartQuotes.runNow(); };
            if ('requestIdleCallback' in window) {
                requestIdleCallback(run, { timeout: 2000 });
            } else {
                // slight delay to allow any late DOM insertions
                setTimeout(run, 350);
            }
            if (window.console) console.info('smartQuotes: scheduled run after load.');
        } catch (e) {
            console.error('smartQuotes.runAfterLoad error:', e);
        }
    }
 
    if (document.readyState === 'complete') {
        runAfterLoad();
    } else {
        // Attach to load to guarantee it runs after everything (including ResourceLoader errors)
        window.addEventListener('load', runAfterLoad, false);
        // Also run on DOMContentLoaded as a fallback
        document.addEventListener('DOMContentLoaded', function () {
            // still defer slightly so other inline scripts finish
            setTimeout(runAfterLoad, 150);
        }, false);
    }
 
    // Helpful console test: window.smartQuotes.test()
    window.smartQuotes.test = function (input) {
        input = input || " 'Hello' It's John's 'quote' ";
        var out = convertSingleQuotes(input);
        console.log('smartQuotes.test input:', input);
        console.log('smartQuotes.test output:', out);
        return out;
    };
 
})(window, document);