MediaWiki:Common.js: Difference between revisions

no edit summary
No edit summary
No edit summary
Line 782: Line 782:




// MediaWiki:Common.js
(function (window, document) {
// Smart single-quote substitution (fixed) - ver 1.3
    'use strict';


( function ( window, document, mw ) {
    // Small API object for testing/rerun
    'use strict';
    window.smartQuotes = window.smartQuotes || {};


     // Minimal safety: if mw is missing, bail out gracefully
     // Convert single quotes in a text string to smart quotes
     if ( typeof mw === 'undefined' ) {
     function convertSingleQuotes(text) {
         console.warn('smartQuotes: mw is not available; aborting.');
        if (!text || text.indexOf("'") === -1) return text;
         return;
        // Protect contractions/possessives like don't or John's
        text = text.replace(/([A-Za-z0-9])'([A-Za-z0-9])/g, "$1’$2");
        // Opening single quote when at start or after whitespace/open punctuation
         text = text.replace(/(^|[\s\(\[\{\<\u2014\u2013"“'«])'(?=\S)/g, "$1‘");
        // Remaining single quotes are closing
        text = text.replace(/'/g, "’");
         return text;
     }
     }


     // Ensure we load when ResourceLoader is ready
     // Walk text nodes under root and replace text content (skip code-like tags)
    mw.loader.using([], function () {
    function walkAndReplace(root) {
 
        if (!root) return;
        // Utility: are we anonymous? (avoids using mw.user.isAnon)
        try {
        function isAnon() {
            var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
            var wgUserId = mw.config.get('wgUserId');
            var node;
            // wgUserId is 0 or null for anonymous users in many setups
            var blacklist = new Set(["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA", "NOSCRIPT", "MATH", "INPUT", "TEXTAREA"]);
            return !wgUserId || wgUserId === 0;
            while ((node = walker.nextNode())) {
        }
                var parent = node.parentNode;
 
                if (!parent) continue;
        // Convert single quotes -> smart quotes in a string
                // Skip nodes with blacklisted ancestor
        function convertSingleQuotes(text) {
                var anc = parent, skip = false;
            if (!text || text.indexOf("'") === -1) return text;
                while (anc && anc.nodeType === 1) {
            // Protect contractions/possessives like don't or John's
                    if (blacklist.has(anc.nodeName)) { skip = true; break; }
            text = text.replace(/([A-Za-z0-9])'([A-Za-z0-9])/g, "$1’$2");
                    anc = anc.parentNode;
            // Opening single quote when at start or after whitespace or opening punctuation
            text = text.replace(/(^|[\s\(\[\{\<\u2014\u2013"“'«])'(?=\S)/g, "$1‘");
            // Remaining single quotes are closing
            text = text.replace(/'/g, "’");
            return text;
        }
 
        // Walk text nodes under root and replace text content (skip code-like tags)
        function walkAndReplace(root) {
            try {
                if (!root) return;
                var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
                var node;
                var blacklist = new Set(["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA", "NOSCRIPT", "MATH", "INPUT"]);
 
                while ((node = walker.nextNode())) {
                    var parent = node.parentNode;
                    if (!parent) continue;
                    // skip if any ancestor is blacklisted
                    var anc = parent, skip = false;
                    while (anc && anc.nodeType === 1) {
                        if (blacklist.has(anc.nodeName)) { skip = true; break; }
                        anc = anc.parentNode;
                    }
                    if (skip) continue;
                    var original = node.nodeValue;
                    var replaced = convertSingleQuotes(original);
                    if (replaced !== original) node.nodeValue = replaced;
                 }
                 }
             } catch (e) {
                if (skip) continue;
                var original = node.nodeValue;
                var replaced = convertSingleQuotes(original);
                if (replaced !== original) node.nodeValue = replaced;
             }
        } catch (e) {
            // Defensive: log but don't throw
            if (window.console && window.console.error) {
                 console.error('smartQuotes.walkAndReplace error:', e);
                 console.error('smartQuotes.walkAndReplace error:', e);
             }
             }
         }
         }
    }


        // Process initial content
    // Process main content area (parser output) then body fallback
        function processAll() {
    function processOnce() {
            try {
        var root = document.querySelector('.mw-parser-output') || document.body;
                var root = document.querySelector('.mw-parser-output') || document.body;
        walkAndReplace(root);
                walkAndReplace(root);
    }
                console.info('smartQuotes: processAll completed.');
            } catch (e) {
                console.error('smartQuotes.processAll error:', e);
            }
        }


        // Observer for dynamic content
    // Observe for dynamic/AJAX content
        function initObserver() {
    function initObserver() {
            try {
        try {
                var observer = new MutationObserver(function (mutations) {
            var obs = new MutationObserver(function (mutations) {
                    mutations.forEach(function (m) {
                mutations.forEach(function (m) {
                        if (m.addedNodes && m.addedNodes.length) {
                    m.addedNodes && m.addedNodes.forEach(function (n) {
                            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);
                            });
                        }
                     });
                     });
                 });
                 });
                observer.observe(document.body, { childList: true, subtree: true });
            });
                window.smartQuotes = window.smartQuotes || {};
            obs.observe(document.body, { childList: true, subtree: true });
                window.smartQuotes._observer = observer;
            window.smartQuotes._observer = obs;
                console.info('smartQuotes: MutationObserver inited.');
        } catch (e) {
            } catch (e) {
            console.error('smartQuotes.initObserver error:', e);
                console.error('smartQuotes.initObserver error:', e);
            }
         }
         }
    }


        // Expose test function for console verification
    // Public API to run now (useful to trigger from console)
        window.smartQuotes = window.smartQuotes || {};
    window.smartQuotes.runNow = function () {
        window.smartQuotes.test = function (input) {
        processOnce();
            input = input || " 'Hello' It's John's 'quote' ";
        if (!window.smartQuotes._observer) initObserver();
            var out = convertSingleQuotes(input);
        return true;
            console.log('smartQuotes.test input:', input);
    };
            console.log('smartQuotes.test output:', out);
            return out;
        };


        // init after DOM ready
    // Run after full window load — this avoids ResourceLoader bundle errors
        function initWhenReady() {
    function runAfterLoad() {
             if (document.readyState === 'complete' || document.readyState === 'interactive') {
        try {
                 processAll();
             // Use requestIdleCallback if available for low priority, fallback to setTimeout
                initObserver();
            var run = function () { window.smartQuotes.runNow(); };
            if ('requestIdleCallback' in window) {
                 requestIdleCallback(run, { timeout: 2000 });
             } else {
             } else {
                 document.addEventListener('DOMContentLoaded', function () {
                 // slight delay to allow any late DOM insertions
                    processAll();
                setTimeout(run, 350);
                    initObserver();
                });
             }
             }
            if (window.console) console.info('smartQuotes: scheduled run after load.');
        } catch (e) {
            console.error('smartQuotes.runAfterLoad error:', e);
         }
         }
    }


         initWhenReady();
    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);
    }


     }); // mw.loader.using
     // 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, window.mw );
})(window, document);