MediaWiki:Common.js: Difference between revisions

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




// Put this in MediaWiki:Common.js
( function (window, document, mw) {
( function () {
     'use strict';
     'use strict';


     // Convert single quotes in a text string to smart quotes
     // Expose a minimal API for testing
    window.smartQuotes = window.smartQuotes || {};
 
     function convertSingleQuotes(text) {
     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
         // 1) Protect contractions/possessives like "don't" or "John's"
         text = text.replace(/([A-Za-z0-9])'([A-Za-z0-9])/g, "$1’$2");
         text = text.replace(/([A-Za-z0-9])'([A-Za-z0-9])/g, "$1’$2");
 
         // Opening quote when at start or after whitespace or open punctuation
         // 2) Replace opening single quotes:
        // Replace a straight quote that occurs at start of string or after whitespace/opening punctuation
        // with an opening single quote ‘
         text = text.replace(/(^|[\s\(\[\{\<\u2014\u2013"“'«])'(?=\S)/g, "$1‘");
         text = text.replace(/(^|[\s\(\[\{\<\u2014\u2013"“'«])'(?=\S)/g, "$1‘");
 
         // Remaining single quotes are closing
         // 3) Remaining single quotes are closing quotes ’
         text = text.replace(/'/g, "’");
         text = text.replace(/'/g, "’");
         return text;
         return text;
     }
     }


    // Walk text nodes under root and replace text content
     function walkAndReplace(root) {
     function walkAndReplace(root) {
         if (!root) return;
         if (!root) return;
         var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
         var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
         var node;
         var node;
        // Tags to ignore entirely
         var blacklist = new Set(["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA", "NOSCRIPT", "MATH", "INPUT"]);
         var blacklist = new Set(["CODE", "PRE", "SCRIPT", "STYLE", "TEXTAREA", "NOSCRIPT", "MATH", "INPUT"]);


Line 816: Line 809:
             if (!parent) continue;
             if (!parent) continue;


             // If any ancestor is blacklisted, skip this node
             // Skip nodes with blacklisted ancestor
             var skip = false;
             var anc = parent, skip = false;
            var anc = parent;
             while (anc && anc.nodeType === 1) {
             while (anc && anc !== document && anc.nodeType === 1) {
                 if (blacklist.has(anc.nodeName)) { skip = true; break; }
                 if (blacklist.has(anc.nodeName)) { skip = true; break; }
                 anc = anc.parentNode;
                 anc = anc.parentNode;
Line 831: Line 823:
     }
     }


     // Run on initial content
     // Kick off on initial DOM and expose functions for debugging
     function processAll() {
     function processAll() {
         walkAndReplace(document.body);
         try {
            var root = document.querySelector('.mw-parser-output') || document.body;
            walkAndReplace(root);
            console.info('smartQuotes: processAll completed.');
        } catch (e) {
            console.error('smartQuotes processAll error:', e);
        }
     }
     }


     // Handle dynamic content (AJAX navigation)
     // MutationObserver for dynamic content
     function initObserver() {
     function initObserver() {
         var observer = new MutationObserver(function(mutations) {
         try {
            mutations.forEach(function(m) {
            var observer = new MutationObserver(function (mutations) {
                if (m.addedNodes && m.addedNodes.length) {
                mutations.forEach(function (m) {
                    m.addedNodes.forEach(function(n) {
                    if (m.addedNodes && m.addedNodes.length) {
                        if (n.nodeType === 1) { // element
                        m.addedNodes.forEach(function (n) {
                            walkAndReplace(n);
                            if (n.nodeType === 1) walkAndReplace(n);
                        } else if (n.nodeType === 3) { // text node
                            else if (n.nodeType === 3 && n.parentNode) walkAndReplace(n.parentNode);
                            // handle single text node's parent element
                         });
                            var parent = n.parentNode;
                     }
                            if (parent) walkAndReplace(parent);
                 });
                         }
                     });
                 }
             });
             });
        });
            observer.observe(document.body, { childList: true, subtree: true });
        observer.observe(document.body, { childList: true, subtree: true });
            window.smartQuotes._observer = observer;
            console.info('smartQuotes: MutationObserver inited.');
        } catch (e) {
            console.error('smartQuotes observer error:', e);
        }
    }
 
    // Expose a test utility to call from the console
    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;
    };
 
    // Init after ResourceLoader and DOM ready
    function init() {
        processAll();
        initObserver();
     }
     }


    // Use MediaWiki hooks if available, otherwise fallback to DOMContentLoaded
     if (mw && mw.loader) {
     if (window.mw && mw.hook) {
         mw.loader.using([], function () {
         mw.hook('wikipage.content').add(function($content) { walkAndReplace($content[0]); });
            if (document.readyState === 'complete' || document.readyState === 'interactive') {
        mw.loader.using(['mediawiki.util']).then(function(){ processAll(); initObserver(); });
                init();
            } else {
                document.addEventListener('DOMContentLoaded', init);
            }
        });
     } else {
     } else {
         document.addEventListener('DOMContentLoaded', function () { processAll(); initObserver(); });
         // fallback
        if (document.readyState === 'complete' || document.readyState === 'interactive') init();
        else document.addEventListener('DOMContentLoaded', init);
     }
     }


})();
    // Small heartbeat so we can see if script runs at all
    console.info('smartQuotes script loaded.');
 
})(window, document, window.mw);