跳转到内容

User:CHih-See Hsie/Gadget-text-spacing.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
 * Forked from MediaWiki:Gadget-text-spacing.js
 * This gadget automatically adjust spacing between Chinese with English,
 * with symbols and with numbers.
 *
 * @author [[User:Diskdance]] <https://w.wiki/5F6e>
 * @license BSD-3-Clause
 * @see https://github.com/diskdance/gadget-text-spacing
 */
// <nowiki>
(function () {
    'use strict';

    const pendingActions = new WeakMap();
    // Optimization: lazily execute pending actions once an element is visible
    const observer = new IntersectionObserver(onIntersection);
    function onIntersection(entries) {
        entries.forEach((entry) => {
            if (entry.isIntersecting) {
                const element = entry.target;
                observer.unobserve(element);
                const callbacks = pendingActions.get(element);
                if (callbacks !== undefined) {
                    while (true) {
                        const callback = callbacks.shift(); // FIFO
                        if (callback === undefined) {
                            break;
                        }
                        callback(element);
                    }
                }
            }
        });
    }
    function queueDomMutation(element, callback) {
        var _a;
        if (!pendingActions.has(element)) {
            pendingActions.set(element, []);
        }
        (_a = pendingActions.get(element)) === null || _a === void 0 ? void 0 : _a.push(callback);
        observer.observe(element);
    }

    function isInlineHTMLElement(node) {
        return node instanceof HTMLElement
            && window.getComputedStyle(node).display.includes('inline');
    }
    function isTextNode(node) {
        return node.nodeType === Node.TEXT_NODE;
    }
    function isVisible(element) {
        const style = window.getComputedStyle(element);
        return style.display !== 'none'
            && !['hidden', 'collapse'].includes(style.visibility)
            && parseFloat(style.opacity) > 0;
    }
    function getNodeText(node) {
        return node instanceof HTMLElement ? node.innerText : node.data;
    }
    /**
     * Split a string before an array of indexes.
     *
     * For example,
     * ```
     * splitAtIndexes('123456789', [3, 5, 7]);
     * ```
     * results in
     * ```
     * ['123', '45', '67', '89']
     * ```
     *
     * Note that empty string are included:
     * ```
     * splitAtIndexes('123456789', [0, 9]);
     * ```
     * results in
     * ```
     * ['', '123456789', '']
     * ```
     *
     * Indexes that are negative or greater than the length of the string are ignored.
     *
     * @param str string to split
     * @param indexes indexes
     * @returns splitted string fragments
     */
    function splitAtIndexes(str, indexes) {
        const result = [];
        const normalizedIndexes = [
            // Remove duplications and sort in ascending order
            ...new Set(indexes
                .sort((a, b) => a - b)
                .filter((i) => i >= 0 && i <= str.length)),
            str.length,
        ];
        for (let i = 0; i < normalizedIndexes.length; i++) {
            const slice = str.slice(normalizedIndexes[i - 1], normalizedIndexes[i]);
            result.push(slice);
        }
        return result;
    }

    const REGEX_RANGE_CHINESE = '(?:[\\u2E80-\\u2E99\\u2E9B-\\u2EF3\\u2F00-\\u2FD5\\u3005\\u3007\\u3021-\\u3029\\u3038-\\u303B\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFA6D\\uFA70-\\uFAD9]|\\uD81B[\\uDFE2\\uDFE3\\uDFF0\\uDFF1]|[\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879\\uD880-\\uD883][\\uDC00-\\uDFFF]|\\uD869[\\uDC00-\\uDEDF\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF38\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0]|\\uD87E[\\uDC00-\\uDE1D]|\\uD884[\\uDC00-\\uDF4A])';
    const REGEX_RANGE_OTHER_LEFT = '(?:[a-zA-Z0-9§®+<=>,.?!;:\\u0029\\u005D\\u007D\\u00A2-\\u00A5\\u00A9-\\u00AC\\u00B0-\\u00B6\\u00B8-\\u00BE\\u00C0-\\u1FFF\\u2070-\\u2E7F]|[\\uA4D0-\\uABFF])';
    const REGEX_RANGE_OTHER_RIGHT = '(?:[a-zA-Z0-9§®+<=>\\u0028\\u005B\\u007B\\u00A2-\\u00A5\\u00A9-\\u00AC\\u00B0-\\u00B6\\u00B8-\\u00BE\\u00C0-\\u1FFF\\u2070-\\u2E7F]|[\\uA4D0-\\uABFF])';
    const REGEX_STR_INTER_SCRIPT = `(?:(${REGEX_RANGE_CHINESE})(?=${REGEX_RANGE_OTHER_RIGHT})|(${REGEX_RANGE_OTHER_LEFT})(?=${REGEX_RANGE_CHINESE}))`;
    const SPACE = '\u200A';
    const WRAPPER_CLASS = 'gadget-space';
    const SELECTOR_ALLOWED = [
        'a', 'abbr', 'article', 'aside', 'b',
        'bdi', 'big', 'blockquote', 'button',
        'caption', 'center', 'cite', 'data',
        'dd', 'del', 'details', 'dfn', 'div',
        'dt', 'em', 'figcaption', 'footer',
        'h1', 'h2', 'h3', 'h4', 'h5', 'header',
        'i', 'ins', 'label', 'legend', 'li',
        'main', 'mark', 'option', 'p', 'q',
        'ruby', 's', 'section', 'small', 'span',
        'strike', 'strong', 'sub', 'summary',
        'sup', 'td', 'th', 'time', 'u',
    ];
    const SELECTOR_BLOCKED = [
        'code', 'kbd', 'pre', 'rp', 'rt',
        'samp', 'textarea', 'var',
        // Elements with this class are excluded
        '.gadget-nospace',
        // Editable elements
        '[contenteditable="true"]',
        // ACE editor content
        '.ace_editor',
        // Visual Editor (and 2017 Wikitext Editor) content & diff
        '.ve-ui-surface',
        '.ve-init-mw-diffPage-diff',
        // Diff
        '.diff-context',
        '.diff-addedline',
        '.diff-deletedline',
        // Diff (inline mode)
        '.mw-diff-inline-added',
        '.mw-diff-inline-deleted',
        '.mw-diff-inline-moved',
        '.mw-diff-inline-changed',
        '.mw-diff-inline-context',
    ];
    // FIXME: Use :is() in the future once it has better browser compatibility
    const SELECTOR = SELECTOR_ALLOWED
        .map((allowed) => `${allowed}:not(${SELECTOR_BLOCKED
    .flatMap((blocked) => 
// Not include itself if it is a tag selector
blocked[0].match(/[a-z]/i) ? `${blocked} *` : [blocked, `${blocked} *`])
    .join()})`)
        .join();
    function getLeafElements(parent) {
        const candidates = parent.querySelectorAll(SELECTOR);
        const result = [];
        if (parent.matches(SELECTOR)) {
            result.push(parent);
        }
        for (const candidate of candidates) {
            for (const childNode of candidate.childNodes) {
                if (isTextNode(childNode)) {
                    result.push(candidate);
                    break;
                }
            }
        }
        return result;
    }
    function getNextVisibleSibling(node) {
        let currentNode = node;
        // Use loops rather than recursion for better performance
        while (true) {
            const candidate = currentNode.nextSibling;
            if (candidate === null) {
                const parent = currentNode.parentElement;
                if (parent === null) {
                    // Parent is Document, so no visible sibling
                    return null;
                }
                // Bubble up to its parent and get its sibling
                currentNode = parent;
                continue;
            }
            if (!(candidate instanceof HTMLElement || candidate instanceof Text)) {
                // Comments, SVGs, etc.: get its sibling as result
                currentNode = candidate;
                continue;
            }
            if (candidate instanceof HTMLElement) {
                if (!isVisible(candidate)) {
                    // Invisible: recursively get this element's next sibling
                    currentNode = candidate;
                    continue;
                }
                if (!isInlineHTMLElement(candidate)) {
                    // Next sibling is not inline (at next line), so no siblings
                    return null;
                }
            }
            if (candidate instanceof Text && candidate.data.trim() === '') {
                // Skip empty Text nodes (e.g. line breaks)
                currentNode = candidate;
                continue;
            }
            return candidate;
        }
    }
    function createSpacingWrapper(str) {
        const span = document.createElement('span');
        span.className = WRAPPER_CLASS;
        span.innerText = str.slice(-1);
        return [str.slice(0, -1), span];
    }
    function adjustSpacing(element) {
        var _a;
        // Freeze NodeList in advance
        const childNodes = [...element.childNodes];
        const textSpacingPosMap = new Map();
        for (const child of childNodes) {
            if (child instanceof Text) {
                const nextSibling = getNextVisibleSibling(child);
                let testString = getNodeText(child);
                if (nextSibling !== null) {
                    // Append first character to detect script intersection
                    testString += (_a = getNodeText(nextSibling)[0]) !== null && _a !== void 0 ? _a : '';
                }
                const indexes = [];
                // Global regexps are stateful so do initialization in each loop
                const regexTextNodeData = new RegExp(REGEX_STR_INTER_SCRIPT, 'g');
                while (true) {
                    const match = regexTextNodeData.exec(testString);
                    if (match === null) {
                        break;
                    }
                    indexes.push(match.index + 1); // +1 to match script boundary
                }
                if (indexes.length === 0) {
                    // Optimization: skip further steps
                    // Also prevent unnecessary mutation, which will be detected by MutationObserver,
                    // resulting in infinite loops
                    continue;
                }
                textSpacingPosMap.set(child, indexes);
            }
        }
        // Schedule DOM mutation to prevent forced reflows
        queueDomMutation(element, () => {
            for (const [node, indexes] of textSpacingPosMap) {
                const text = node.data;
                const fragments = splitAtIndexes(text, indexes);
                const replacement = fragments
                    .slice(0, -1)
                    .flatMap((fragment) => createSpacingWrapper(fragment));
                replacement.push(fragments.slice(-1)[0]);
                node.replaceWith(...replacement);
            }
        });
    }
    function addSpaceToString(str) {
        const regex = new RegExp(REGEX_STR_INTER_SCRIPT, 'g');
        return str.replace(regex, `$1$2${SPACE}`);
    }

    function run(element) {
        const leaves = getLeafElements(element);
        leaves.forEach((leaf) => {
            adjustSpacing(leaf);
        });
    }
    const mutationObserver = new MutationObserver((records) => {
        records.forEach((record) => {
            if (record.type === 'childList') {
                const nodes = [...record.addedNodes];
                // Exclude mutations caused by adjustSpacing() to prevent infinite loops
                // Typically they will contain nodes with class WRAPPER_CLASS
                if (!nodes.some((node) => node instanceof HTMLElement
                    && node.classList.contains(WRAPPER_CLASS))) {
                    // Optimization: prevent forced reflows
                    requestAnimationFrame(() => {
                        nodes.forEach((node) => {
                            if (node instanceof HTMLElement) {
                                run(node);
                            }
                            else if (node instanceof Text) {
                                const parentElement = node.parentElement;
                                if (parentElement !== null) {
                                    run(parentElement);
                                }
                            }
                        });
                    });
                }
            }
        });
    });
    function main() {
        document.title = addSpaceToString(document.title);
        // Watch for added nodes
        mutationObserver.observe(document.body, { subtree: true, childList: true });
        run(document.body);
    }
    $(main);

})();
// </nowiki>