MediaWiki:Gadget-VariantAlly.js

注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**!
 *  _________________________________________________________________________________
 * |                                                                                 |
 * |                      === WARNING: GLOBAL GADGET FILE ===                        |
 * |                    Changes to this page affect many users.                      |
 * |  Please discuss changes on the talk page, [[WP:VPT]] or GitHub before editing.  |
 * |_________________________________________________________________________________|
 *
 * Built from GitHub repository (https://github.com/wikimedia-gadgets/VariantAlly), you should not make
 * changes directly here.
 *
 * See https://github.com/diskdance/wikimedia-gadgets/blob/main/CONTRIBUTING.md for build instructions.
 */
// <nowiki>
'use strict';

// Including:
// - w.wiki (preserve short link destination)
const BLOCKED_REFERRER_HOST = /^w\.wiki$/i;
function isLoggedIn() {
    return mw.config.exists('wgUserId');
}
/**
 * Check whether referrer originates from the same domain.
 */
function isReferrerSelf() {
    try {
        return new URL(document.referrer).hostname === location.hostname;
    }
    catch (_a) {
        // Invalid URL
        return false;
    }
}
function isReferrerBlocked() {
    try {
        return BLOCKED_REFERRER_HOST.test(new URL(document.referrer).hostname);
    }
    catch (_a) {
        // Invalid URL
        return false;
    }
}
function isViewingPage() {
    return mw.config.get('wgAction') === 'view';
}
/**
 * Check whether the current language (set in user preference or by ?uselang=xxx)
 * is Chinese or not.
 */
function isLangChinese() {
    return mw.config.get('wgUserLanguage').startsWith('zh');
}
function isWikitextPage() {
    return mw.config.get('wgCanonicalNamespace') !== 'Special'
        && mw.config.get('wgPageContentModel') === 'wikitext';
}

const LOCAL_VARIANT_KEY = 'va-var';
const OPTOUT_KEY = 'va-optout';
const VALID_VARIANTS = [
    'zh-cn',
    'zh-sg',
    'zh-my',
    'zh-tw',
    'zh-hk',
    'zh-mo',
];
const VARIANTS = [
    'zh',
    'zh-hans',
    'zh-hant',
    ...VALID_VARIANTS,
];
const EXT_VARIANTS = [
    'zh-hans-cn',
    'zh-hans-sg',
    'zh-hans-my',
    'zh-hant-tw',
    'zh-hant-hk',
    'zh-hant-mo',
    ...VARIANTS,
];
// Some browsers (e.g. Firefox Android) may return such languages
const EXT_MAPPING = {
    'zh-hans-cn': 'zh-cn',
    'zh-hans-sg': 'zh-sg',
    'zh-hans-my': 'zh-my',
    'zh-hant-tw': 'zh-tw',
    'zh-hant-hk': 'zh-hk',
    'zh-hant-mo': 'zh-mo',
};
function isVariant(str) {
    return VARIANTS.includes(str);
}
function isValidVariant(str) {
    return VALID_VARIANTS.includes(str);
}
function isExtVariant(str) {
    return EXT_VARIANTS.includes(str);
}
/**
 * Maps additional lang codes to standard variants.
 *
 * @returns standard variant
 */
function normalizeVariant(extVariant) {
    var _a;
    return (_a = EXT_MAPPING[extVariant]) !== null && _a !== void 0 ? _a : extVariant;
}
/**
 * Get current variant of the page (don't be misled by config naming).
 * @returns variant, null for non-wikitext page (but NOT all such pages returns null!)
 */
function getPageVariant() {
    const result = mw.config.get('wgUserVariant');
    return result !== null && isExtVariant(result) ? normalizeVariant(result) : null;
}
/**
 * Get account variant.
 * @returns account variant, null for anonymous user
 */
function getAccountVariant() {
    if (isLoggedIn()) {
        const result = mw.user.options.get('variant');
        return isExtVariant(result) ? normalizeVariant(result) : null;
    }
    return null;
}
function getLocalVariant() {
    const result = localStorage.getItem(LOCAL_VARIANT_KEY);
    if (result === null || !isExtVariant(result)) {
        return null;
    }
    return normalizeVariant(result);
}
/**
 * Return browser language if it's a Chinese variant.
 * @returns browser variant
 */
function getBrowserVariant() {
    var _a;
    return (_a = navigator.languages
        .map((lang) => lang.toLowerCase())
        .filter(isExtVariant)
        .map(normalizeVariant)
        .find(isVariant)) !== null && _a !== void 0 ? _a : null;
}
/**
 * Get the "natural" variant inferred by MediaWiki for anonymous users
 * when the link doesn't specify a variant.
 *
 * Used in link normalization.
 *
 * FIXME: Old Safari is known to break this method.
 * User reported that on an iOS 14 device with Chinese language and Singapore region settings,
 * Accept-Language is zh-cn (thus inferred by MediaWiki) but this method returns zh-sg.
 *
 * @returns variant
 */
function getMediaWikiVariant() {
    var _a;
    return (_a = getAccountVariant()) !== null && _a !== void 0 ? _a : getBrowserVariant();
}
/**
 * Calculate preferred variant from browser variant, local variant and account variant.
 *
 * Priority: account variant > browser variant > local variant
 *
 * @returns preferred variant
 */
function calculatePreferredVariant() {
    return [getAccountVariant(), getBrowserVariant(), getLocalVariant()]
        .map((variant) => variant !== null && isValidVariant(variant) ? variant : null)
        .reduce((prev, curr) => prev !== null && prev !== void 0 ? prev : curr);
}
function setLocalVariant(variant) {
    localStorage.setItem(LOCAL_VARIANT_KEY, variant);
}
function setOptOut() {
    localStorage.setItem(OPTOUT_KEY, '');
}
function isOptOuted() {
    return localStorage.getItem(OPTOUT_KEY) !== null;
}

const REGEX_WIKI_URL = /^\/(?:wiki|zh(?:-\w+)?)\//i;
const REGEX_VARIANT_URL = /^\/zh(?:-\w+)?\//i;
const VARIANT_PARAM = 'va-variant';
function isEligibleForRewriting(link) {
    try {
        // No rewriting for empty links
        if (link === '') {
            return false;
        }
        const url = new URL(link, location.origin);
        // No rewriting if link itself has variant info
        if (REGEX_VARIANT_URL.test(url.pathname)) {
            return false;
        }
        if (url.searchParams.has('variant')) {
            return false;
        }
        // No rewriting for foreign origin URLs
        // Note that links like javascript:void(0) are blocked by this
        if (url.host !== location.host) {
            return false;
        }
        return true;
    }
    catch (_a) {
        return false;
    }
}
function rewriteLink(link, variant) {
    try {
        const normalizationTargetVariant = getMediaWikiVariant();
        const url = new URL(link, location.origin);
        const pathname = url.pathname;
        const searchParams = url.searchParams;
        if (REGEX_WIKI_URL.test(pathname)) {
            url.pathname = `/${variant}/${url.pathname.replace(REGEX_WIKI_URL, '')}`;
            searchParams.delete('variant'); // For things like /zh-cn/A?variant=zh-hk
        }
        else {
            searchParams.set('variant', variant);
        }
        if (variant === normalizationTargetVariant) {
            // Normalize the link.
            //
            // For example, for link /zh-tw/Title and normalization variant zh-tw, the result is /wiki/Title,
            // while for the same link and normalization variant zh-cn, the result is /zh-tw/Title (unchanged).
            url.pathname = url.pathname.replace(REGEX_WIKI_URL, '/wiki/');
            url.searchParams.delete('variant');
        }
        const result = url.toString();
        return result;
    }
    catch (_a) {
        return link;
    }
}
function redirect(preferredVariant, options = {}) {
    var _a;
    const origLink = (_a = options.link) !== null && _a !== void 0 ? _a : location.href;
    const newLink = rewriteLink(origLink, preferredVariant);
    // Prevent infinite redirects
    // This could happen occasionally, see getMediaWikiVariant()'s comments
    if (options.forced || newLink !== location.href) {
        // Use replace() to prevent navigating back
        location.replace(newLink);
    }
}
function checkThisPage(preferredVariant, pageVariant) {
    if (pageVariant === preferredVariant) {
        return;
    }
    const redirectionOrigin = mw.config.get('wgRedirectedFrom');
    if (redirectionOrigin === null) {
        redirect(preferredVariant);
        return;
    }
    // If current page is redirected from another page, rewrite link to point to
    // the original redirect so the "redirected from XXX" hint is correctly displayed
    // Use URL to reserve other parts of the link
    const redirectionURL = new URL(location.href);
    redirectionURL.pathname = `/wiki/${redirectionOrigin}`;
    redirect(preferredVariant, { link: redirectionURL.toString() });
}
function rewriteAnchors(variant) {
    ['click', 'auxclick', 'dragstart'].forEach((name) => {
        document.addEventListener(name, (ev) => {
            const target = ev.target;
            if (target instanceof Element) {
                // Do not write <a> with hash only href or no href
                // which is known to cause breakage in e.g. Visual Editor
                const anchor = target.closest('a[href]:not([href^="#"])');
                if (anchor !== null) {
                    const origLink = anchor.href;
                    if (!isEligibleForRewriting(origLink)) {
                        return;
                    }
                    const newLink = rewriteLink(origLink, variant);
                    if (newLink === origLink) {
                        return;
                    }
                    // Browser support: Safari < 14
                    // Fail silently when DragEvent is not present
                    if (window.DragEvent && ev instanceof DragEvent && ev.dataTransfer) {
                        // Modify drag data directly because setting href has no effect in drag event
                        ev.dataTransfer.types.forEach((type) => {
                            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                            ev.dataTransfer.setData(type, newLink);
                        });
                    }
                    else {
                        // Use a mutex to avoid being overwritten by overlapped handler calls
                        if (anchor.dataset.vaMutex === undefined) {
                            anchor.dataset.vaMutex = '';
                        }
                        anchor.href = newLink;
                        // HACK: workaround popups not working on modified links
                        // Add handler to <a> directly so it was triggered before anything else
                        ['mouseover', 'mouseleave', 'keyup'].forEach((innerName) => {
                            anchor.addEventListener(innerName, (innerEv) => {
                                if (anchor.dataset.vaMutex !== undefined) {
                                    anchor.href = origLink;
                                    delete anchor.dataset.vaMutex;
                                }
                            }, { once: true });
                        });
                    }
                }
            }
        });
    });
}
function showVariantPrompt() {
    mw.loader.using('ext.gadget.VariantAllyDialog').then(function(require){return require("ext.gadget.VariantAllyDialog")});
}
/**
 * Set local variant according to URL query parameters.
 *
 * e.g. a URL with ?va-variant=zh-cn will set local variant to zh-cn
 */
function applyURLVariant() {
    const variant = new URL(location.href).searchParams.get(VARIANT_PARAM);
    if (variant !== null && isValidVariant(variant)) {
        setLocalVariant(variant);
    }
}

/**
 * Collect metrics, visible at grafana.wikimedia.org
 *
 * @param name metric name
 */
function stat(name) {
    mw.track(`counter.gadget_VariantAlly.${name}`);
}

function main() {
    // Manually opt outed users
    if (isOptOuted()) {
        return;
    }
    if (isLoggedIn()) {
        return;
    }
    // Non-Chinese pages/users
    if (!isLangChinese()) {
        return;
    }
    applyURLVariant();
    const preferredVariant = calculatePreferredVariant();
    if (preferredVariant !== null) {
        // Optimistically set local variant to be equal to browser variant
        // In case the user's browser language becomes invalid in the future,
        // this reduces the possibility to show prompt to disrupt users.
        setLocalVariant(preferredVariant);
    }
    const pageVariant = getPageVariant();
    // Non-article page (JS/CSS pages, Special pages etc.)
    if (pageVariant === null || !isWikitextPage()) {
        // Such page can't have variant, but preferred variant may be available
        // So still rewrite links
        if (preferredVariant !== null) {
            rewriteAnchors(preferredVariant);
        }
        return;
    }
    // Preferred variant unavailable
    if (preferredVariant === null) {
        if (isViewingPage()) {
            showVariantPrompt();
            return;
        }
        return;
    }
    if (isReferrerBlocked()) {
        rewriteAnchors(preferredVariant);
        return;
    }
    // On-site navigation to links ineligible for writing
    // The eligibility check is require because user may click on a link with variant intentionally
    // e.g. variant dropdown and {{Variant-cnhktwsg}}
    if (isReferrerSelf() && !isEligibleForRewriting(location.href)) {
        rewriteAnchors(preferredVariant);
        return;
    }
    checkThisPage(preferredVariant, pageVariant);
    rewriteAnchors(preferredVariant);
}
main();

exports.redirect = redirect;
exports.setLocalVariant = setLocalVariant;
exports.setOptOut = setOptOut;
exports.stat = stat;
// </nowiki>