MediaWiki:Gadget-MarkAdmins.js

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

/**
 *  Flag administrators and special user group members with a letter
 *  in parenthesis behind links that go into their user namespace.
 *  E.g. Didym -> Didym (A)
 *
 *  @license
 *  @link https://commons.wikimedia.org/wiki/MediaWiki:Gadget-markAdmins.js
 *
 *  @author Euku - 2005, PDD, Littl, Guandalug
 *  @author Didym - 2014
 *  @author Rillke <https://blog.rillke.com> - 2014
 *  @contributor Perhelion - 2017
 *
 *  @author Dragoniez - 2023, radically modified the original
 *  This modified version differs from the original in three main respects:
 *   - Does not create a new node in user link anchor tags. It only assigns
 *     a CSS class to the relevant tags and shows group information as a
 *     pseudo-element of the class (presumably faster, and $(anchor).text()
 *     doesn't include group notations). 
 *   - Does not suppport configurations via user common.js. Instead, it
 *     provides configurations on [[Special:MarkAdminsConfig]].
 *   - User contribs links are also marked.
 * 
 *  @requires MediaWiki:Gadget-MarkAdmins-data.json
 *  @requires MediaWiki:Gadget-MarkAdmins-updater.js
 */
// <nowiki>
'use strict';

(function(mw, $) {

var MarkAdmins = mw.libs.MarkAdmins = {
    images: {
        loading: '<img src="//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; height: 1em; border: 0;">',
        check: '<img src="//upload.wikimedia.org/wikipedia/commons/f/fb/Yes_check.svg" style="vertical-align: middle; height: 1em; border: 0;">',
        cross: '<img src="//upload.wikimedia.org/wikipedia/commons/a/a2/X_mark.svg" style="vertical-align: middle; height: 1em; border: 0;">'
    },
    config: {},
    defaults: {
        groups: {
            'sysop': {
                label: 'A',
                localized: '管理者',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:管理者',
                enabled: true
            },
            'suppress': {
                label: 'OS',
                localized: 'オーバーサイト',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:オーバーサイトの方針',
                enabled: true
            },
            'checkuser': {
                label: 'CU',
                localized: 'チェックユーザー',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:チェックユーザーの方針',
                enabled: true
            },
            'bureaucrat': {
                label: 'B',
                localized: 'ビューロクラット',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:ビューロクラット',
                enabled: true
            },
            'abusefilter': {
                label: 'FE',
                localized: '編集フィルター編集者',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:編集フィルター編集者',
                enabled: true
            },
            'interface-admin': {
                label: 'IA',
                localized: 'インターフェース管理者',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:インターフェース管理者',
                enabled: true
            },
            'accountcreator': {
                label: 'AC',
                localized: 'アカウント作成者',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:アカウント作成者',
                enabled: true
            },
            'bot': {
                label: 'Bot',
                localized: 'ボット',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:Bot',
                enabled: true
            },
            'eliminator': {
                label: 'E',
                localized: '削除者',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:削除者',
                enabled: true
            },
            'rollbacker': {
                label: 'RB',
                localized: '巻き戻し者',
                link: 'https://ja.wikipedia.org/wiki/Wikipedia:巻き戻し者',
                enabled: true
            },
            'founder': {
                label: 'F',
                localized: '創設者',
                link: 'https://meta.wikimedia.org/wiki/Founder/ja',
                enabled: true
            },
            'steward': {
                label: 'S',
                localized: 'スチュワード',
                link: 'https://meta.wikimedia.org/wiki/Stewards/ja',
                enabled: true
            },
            'ombuds': {
                label: 'Omb',
                localized: 'オンブズ委員',
                link: 'https://meta.wikimedia.org/wiki/Ombuds_commission/ja',
                enabled: true
            },
            'staff': {
                label: 'Staff',
                localized: 'スタッフ',
                link: 'https://wikimediafoundation.org/role/staff-contractors/',
                enabled: true
            },
            'sysadmin': {
                label: 'SA',
                localized: 'システム管理者',
                link: 'https://meta.wikimedia.org/wiki/System_administrators/ja',
                enabled: true
            },
            'global-sysop': {
                label: 'GS',
                localized: 'グローバル管理者',
                link: 'https://meta.wikimedia.org/wiki/Global_sysops/ja',
                enabled: true
            },
            'abusefilter-maintainer': {
                label: 'GFE',
                localized: '編集フィルター保守員',
                link: 'https://meta.wikimedia.org/wiki/Abuse_filter_maintainer/ja',
                enabled: true
            },
            'abusefilter-helper': {
                label: 'GFH',
                localized: '編集フィルター閲覧者',
                link: 'https://meta.wikimedia.org/wiki/Abuse_filter_helpers/ja',
                enabled: true
            },
            'global-interface-editor': {
                label: 'GIE',
                localized: 'グローバルインターフェース編集者',
                link: 'https://meta.wikimedia.org/wiki/Interface_editors/ja',
                enabled: true
            },
            'global-bot': {
                label: 'GBot',
                localized: 'グローバルボット',
                link: 'https://meta.wikimedia.org/wiki/Bot_policy/ja#global',
                enabled: true
            },
            'global-deleter': {
                label: 'GE',
                localized: 'グローバル削除者',
                link: 'https://meta.wikimedia.org/wiki/Global_deleters/ja',
                enabled: true
            },
            'global-rollbacker': {
                label: 'GRB',
                localized: 'グローバル巻き戻し者',
                link: 'https://meta.wikimedia.org/wiki/Global_rollback/ja',
                enabled: true
            },
            'vrt-permissions': {
                label: 'VRT',
                localized: '問い合わせ対応ボランティアチーム',
                link: 'https://meta.wikimedia.org/wiki/Volunteer_Response_Team/ja',
                enabled: true
            },
            'global-renamer': {
                label: 'GRN',
                localized: 'グローバル利用者名変更者',
                link: 'https://meta.wikimedia.org/wiki/Global_renamers/ja',
                enabled: true
            },
            'wmf-officeit': {
                label: 'WMF OIT',
                localized: 'WMFオフィスIT',
                link: 'https://meta.wikimedia.org/wiki/Meta:WMF_Office_IT/ja',
                enabled: true
            },
            'wmf-supportsafety': {
                label: 'WMF T&S',
                localized: 'WMFサポートと安全班',
                link: 'https://meta.wikimedia.org/wiki/Meta:WMF_Support_and_Safety/ja',
                enabled: true
            }
        },
        runOn: ['Special', '', 'User', 'User_talk', 'Project', 'File', 'Help', 'Portal', 'プロジェクト'],
        runOnOptions: [
            {
                data: 'Special', // Canonical namespace name
                label: '特別' // Corresponding label to show in multi-select
            },
            {
                data: '',
                label: '(標準)'
            },
            {
                data: 'User',
                label: '利用者'
            },
            {
                data: 'User_talk',
                label: '利用者‐会話'
            },
            {
                data: 'Project',
                label: 'Wikipedia'
            },
            {
                data: 'Project_talk',
                label: 'Wikipedia‐ノート'
            },
            {
                data: 'File',
                label: 'ファイル'
            },
            {
                data: 'File_talk',
                label: 'ファイル‐ノート'
            },
            {
                data: 'MediaWiki',
                label: 'MediaWiki'
            },
            {
                data: 'MediaWiki_talk',
                label: 'MediaWiki‐ノート'
            },
            {
                data: 'Template',
                label: 'Template'
            },
            {
                data: 'Template_talk',
                label: 'Template‐ノート'
            },
            {
                data: 'Help',
                label: 'Help'
            },
            {
                data: 'Help_talk',
                label: 'Help‐ノート'
            },
            {
                data: 'Category',
                label: 'Category'
            },
            {
                data: 'Category_talk',
                label: 'Category‐ノート'
            },
            {
                data: 'Portal',
                label: 'Portal'
            },
            {
                data: 'Portal‐ノート',
                label: 'Portal‐ノート'
            },
            {
                data: 'プロジェクト',
                label: 'プロジェクト'
            },
            {
                data: 'プロジェクト‐ノート',
                label: 'プロジェクト‐ノート'
            },
            {
                data: 'Module',
                label: 'モジュール'
            },
            {
                data: 'Module_talk',
                label: 'モジュール‐ノート'
            },
        ],
        runOnHistory: true,
        runOnTalk: true,
        runOnDiff: true,
        markSubpages: false,
        markMyself: true,
        markContribs: true
    },

    /** 
     * @type {number}
     * @static
     * @readonly
     */
    nsNumber: mw.config.get('wgNamespaceNumber'),

    init: function() {

        var isOnConfigPage = MarkAdmins.nsNumber === -1 && /^(MarkAdminsConfig|MAC)$/i.test(mw.config.get('wgTitle'));
        var dependencies = ['mediawiki.user', 'mediawiki.util', 'mediawiki.api', 'oojs-ui-widgets'];
        if (!isOnConfigPage) dependencies.splice(2, 2);
        $.when(mw.loader.using(dependencies), $.ready).then(function() {
            MarkAdmins.createStyleTag();
            MarkAdmins.loadUpdater();
            MarkAdmins.mergeConfig(isOnConfigPage);
        });

    },

    /** @static */
    createStyleTag: function() {
        var style = document.createElement('style');
        style.textContent =
        // Main pseudo selector to show user groups
        '.adminMark::after {' +
            'content: attr(data-groupmarker);' +
            'font-weight: bold;' +
            'font-size: 85%;' +
            'vertical-align: middle;' +
            'margin-left: 0.3em;' +
        '}' +
        // Config page
        // Headers
        '.mac-config-section-header h4 {' +
            'border-bottom: 1px solid #A2A9B1;' +
            'margin-top: 0.2em;' +
        '}' +
        // Border for config body
        '#mac-body > div {' +
            'border: 1px solid #A2A9B1;' +
        '}' +
        '#mac-body > div:not(:last-child) {' +
            'border-bottom: none;' +
        '}' +
        // Change background color of table rows
        '#mac-config-body table tr:nth-child(odd):not(.mac-config-section-header):not(#mac-config-saving):not(#mac-config-lastsaved) td {' +
            'background: #EBD7C9;' +
        '}' +
        // Right-margin for checkboxes
        '#mac-config-body input[type=checkbox] {' +
            'margin-right: 0.5em;' +
        '}' +
        // "Right-margin" for labels that have text fields on their right
        '.mac-config-textinputlabel {' +
            'display: inline-block;' +
            'width: 8ch;' +
        '}' +
        // Margin for buttons
        '#mac-config-body input[type=button] {' +
            'margin: 0.5em;' +
        '}';
        document.head.appendChild(style);
    },

    /** @static */
    loadUpdater: function() {

        // The updater can be used only by sysops. See the descriptions in the module.
        if (mw.config.get('wgUserGroups').indexOf('sysop') === -1) return;

        try {
            // @ts-ignore "XX is not a module."
            require('./MarkAdmins-updater.js');
        }
        catch(err) {
            $.getScript('https://ja.wikipedia.org/w/index.php?title=MediaWiki:Gadget-MarkAdmins-updater.js&action=raw&ctype=text/javascript');
        }

    },

    /** 
     * @type {string}
     * @static
     * @readonly
     */
    userOptionName: 'userjs-MarkAdminsCfg',

    /** 
     * Stores user group JSON data. This property being null means that the script is loaded externally.
     * @type {Object.<string, Array<string>>|null}
     */
    usersData: (function() {
        try {
            // require() works only when loaded via gadgets-definition
            return require('./MarkAdmins-data.json');
        }
        catch(err) {
            return null;
        }
    })(),

    mergeConfig: function(isOnConfigPage) {

        // Merge new configuration
        var MarkAdminsCfg = mw.user.options.get(MarkAdmins.userOptionName);
        var cfg = $.extend(
            true,
            MarkAdmins.config,
            MarkAdmins.defaults,
            MarkAdminsCfg ? JSON.parse(MarkAdminsCfg) : {}
        );

        if (isOnConfigPage) {
            return MarkAdmins.createConfigPage(cfg);
        } else {
            MarkAdmins.createPortletLink();
        }

        // Namespace run conditions
        if (!(cfg.runOn.indexOf(mw.config.get('wgCanonicalNamespace')) !== -1 ||
            cfg.runOnHistory && mw.config.get('wgAction') === 'history' ||
            cfg.runOnTalk && MarkAdmins.nsNumber % 2 ||
            cfg.runOnDiff && !!mw.util.getParamValue('diff')) ||
            mw.config.get('wgCanonicalSpecialPageName') === 'CentralAuth'
        ) {
            return;
        }

        // Proceed to markup
        if (MarkAdmins.usersData) {
            mw.hook('wikipage.content').add(function($content) {
                MarkAdmins.markUser($content);
            });
        } else {
            $.get('https://ja.wikipedia.org/w/index.php?title=MediaWiki:Gadget-MarkAdmins-data.json&action=raw')
                .then(function(usersData) {
                    if (!usersData) return;
                    usersData = JSON.parse(usersData);
                    MarkAdmins.usersData = usersData;
                    mw.hook('wikipage.content').add(function($content) {
                        MarkAdmins.markUser($content);
                    });
                }).catch(function(err) {
                    console.error(err);
                });
        }

    },

    createConfigPage: function(cfg) {

        document.title = 'MarkAdminsの設定 - Wikipedia';

        // Dividing the source html just because deep nests make it hard to see 
        var createConfigBody = function(tableRowsHtml) {
            var html =
            '<div id="mac-container">' +
                '<div id="mac-body" style="margin: 1em 0.5em 0.5em; padding: 0;">' +
                    '<div id="mac-config-titlebar" style="padding: 0.3em; background: #FEC493;">' +
                        '<b>MarkAdmins config</b>' +
                    '</div>' +
                    '<div id="mac-config-body" style="padding: 0.2em; background: #FFF0E4;">' +
                        // <-- ToC
                        '<table style="width: 100%;">' +
                            '<tbody>' +
                                tableRowsHtml +
                            '</tbody>' +
                        '</table>' +
                    '</div>' +
                '</div>' +
            '</div>';
            return html;
        };

        // Create ToC
        var toc = document.createElement('div');
        toc.id = 'mac-toc';
        toc.style.cssText = 'margin: 0.5em; padding: 0.5em; display: table; background: #F8F9FA; border: 1px solid #A2A9B1;';
        var toctitle = document.createElement('div');
        toc.appendChild(toctitle);
        toctitle.id = 'mac-toctitle';
        var toch2 = document.createElement('b');
        toctitle.appendChild(toch2);
        toch2.appendChild(document.createTextNode('目次'));
        var toctoggle = document.createElement('span');
        toctitle.appendChild(toctoggle);
		toctoggle.id = 'mac-toctoggle';
        toctoggle.style.cssText = 'display: inline-block; margin-left: 1em;';
		toctoggle.appendChild(document.createTextNode('['));
		var toctogglelink = document.createElement('a');
        toctoggle.appendChild(toctogglelink);
		toctogglelink.href = '#mac-tocshowhide';
		toctogglelink.textContent = '隠す';
		toctoggle.appendChild(document.createTextNode(']'));
		var tocol = document.createElement('ol'); // This is where toc items go in
        toc.appendChild(tocol);
        if (mw.config.get('skin') !== 'timeless') tocol.style.marginLeft = '2em';

		toctogglelink.addEventListener('click', function() { // For show/hide
			var $tocol = $(tocol);
			$tocol.toggle();
			if ($tocol.find(':visible').length) {
				toctogglelink.textContent = '隠す';
			} else {
				toctogglelink.textContent = '表示';
			}
		}, false);

        /**
         * @param {string} [text]
         * @param {string|null} [id]
         * @param {string|null} [clss]
         * @param {string|null} [cssText]
         * @returns {string}
         */
        var wrapTextBySpan = function(text, id, clss, cssText) {
            var span = document.createElement('span');
            if (text) span.appendChild(document.createTextNode(text));
            if (id) span.id = id;
            if (clss) span.classList.add(clss);
            if (cssText) span.style.cssText = cssText;
            return span.outerHTML;
        };

        var createSectionHeader, headerCnt = 0, createBooleanOption;
        var tableRowsHtml =
        (createSectionHeader = function(headertext) { // ejs-like notation hard to see? Well, at least easier to grasp the outerHTML!

            var id = 'mac-config-section-'+ (++headerCnt);
            var header =
            '<tr class="mac-config-section-header">' +
                '<td colspan="4">' +
                    '<h4 id="' + id +'">' + headertext + '</h4>' +
                '</td>' +
            '</tr>';

            // For ToC
            var li = document.createElement('li');
            tocol.appendChild(li);
            li.className = 'toclevel-1';
            var sectlink = document.createElement('a');
            li.appendChild(sectlink);
            sectlink.href = '#' + id;
            sectlink.appendChild(document.createTextNode(headertext));

            return header;

        })('利用者グループ') +
        (function() {
            var trs = Object.keys(cfg.groups).map(function(g, i) {
                var gObj = cfg.groups[g];
                var gDefaultObj = MarkAdmins.defaults.groups[g];
                var checked = gObj.enabled ? ' checked' : '';
                var checkboxId = 'mac-config-groupenabled-' + i;
                var html =
                '<tr class="mac-config-usergroup" data-group="' + g + '" data-label="' + gDefaultObj.label + '" data-enabled="' + gDefaultObj.enabled + '">' +
                    '<td>' +
                        '<a href="' + gObj.link + '" target="_blank">' + gObj.localized + '</a>' +
                        '<br/>' +
                        '(' + g + ')' +
                    '</td>' +
                    '<td>' +
                        '<label class="mac-config-textinputlabel">ラベル</label>' +
                        '<input class="mac-config-grouplabel" type="text" value="' + gObj.label + '">' +
                        '<br/>' +
                        '<input class="mac-config-groupenabled" id="' + checkboxId + '" type="checkbox"' + checked + '>' +
                        '<label for="' + checkboxId + '">有効化</label>' +
                    '</td>' +
                    '<td>' +
                        '初期値: ' +
                        wrapTextBySpan(gDefaultObj.label, null, 'mac-config-defaultlabel') +
                        ' (' + wrapTextBySpan(gDefaultObj.enabled ? '有効' : '無効', null, 'mac-config-defaultenabled') + ')' +
                    '</td>' +
                    '<td>' +
                        '<input class="mac-config-reset-this" type="button" value="初期化">' +
                    '</td>' +
                '</tr>';
                return html;
            });
            return trs.join('');
        })() +
        '<tr>' +
            '<td colspan="4">' +
                '<input id="mac-config-usergroup-resetall" class="mac-config-resetall" type="button" value="利用者グループの設定を全て初期化">' +
            '</td>' +
        '</tr>' +
        createSectionHeader('対象ページ') +
        '<tr class="mac-config-targetpage">' +
            '<td colspan="3" id="mac-config-runonnamespace" style="padding-bottom: 0.5em; padding-right: 0.3em;">' +
                '有効化する名前空間 (「ノートページ上でマークアップ」が無効の場合でもここで指定された名前空間が優先されます)' +
            '</td>' +
            '<td>' +
                '<input id="mac-config-runonnamespace-reset" type="button" value="初期化">' +
            '</td>' +
        '</tr>' +
        (createBooleanOption = function(configName, optionLabel) {

            var checked;
            if (typeof cfg[configName] === 'undefined') {
                console.error('MarkAdmins config named "' + configName + '" was not found.');
                return '';
            } else {
                checked = !!cfg[configName] ? ' checked' : '';
            }

            var html =
            '<tr class="mac-config-targetpage" data-enabled="' + MarkAdmins.defaults[configName] + '">' +
                '<td colspan="2">' +
                    '<input id="mac-config-' + configName + '" type="checkbox"' + checked + '>' +
                    '<label for="mac-config-' + configName + '">' + optionLabel + '</label>' +
                '</td>' +
                '<td>' +
                    '初期値: ' + wrapTextBySpan(MarkAdmins.defaults[configName] ? '有効' : '無効', null, 'mac-config-defaultenabled') +
                '</td>' +
                '<td>' +
                    '<input class="mac-config-reset-this" type="button" value="初期化">' +
                '</td>' +
            '</tr>';

            return html;

        })('runOnHistory', '編集履歴上でマークアップ') +
        createBooleanOption('runOnTalk', 'ノートページ上でマークアップ') +
        createBooleanOption('runOnDiff', '差分上でマークアップ') +
        createBooleanOption('markSubpages', 'サブページへのリンクをマークアップ') +
        createBooleanOption('markMyself', '自分のリンクをマークアップ') +
        createBooleanOption('markContribs', '投稿記録へのリンクをマークアップ') +
        '<tr>' +
            '<td colspan="4">' +
                '<input id="mac-config-targetpage-resetall" class="mac-config-resetall" type="button" value="対象ページの設定を全て初期化">' +
            '</td>' +
        '</tr>' +
        createSectionHeader('保存') +
        '<tr>' +
            '<td colspan="4">' +
                '<input id="mac-config-save" type="button" value="設定を保存">' +
                '<input id="mac-config-resetall-do" type="button" value="全ての設定を初期化">' +
            '</td>' +
        '</tr>' +
        '<tr id="mac-config-saving" style="display: none;">' +
            '<td colspan="4"></td>' +
        '</tr>' +
        '<tr id="mac-config-lastsaved" style="display: none;">' +
            '<td colspan="4"></td>' +
        '</tr>';

        // Replace body content. Easier to just replace mw.util.$content[0].innerHTML, but this would remove #p-cactions etc.
        var bodyContent = document.querySelector('.mw-body-content') || mw.util.$content[0];
        bodyContent.innerHTML = createConfigBody(tableRowsHtml);
        var firstHeading = document.querySelector('.mw-first-heading');
        if (firstHeading) { // The innerHTML of .mw-body-content was replaced
            firstHeading.textContent = 'MarkAdminsの設定';
        } else { // The innerHTML of mw.util.$content[0] was replaced (in this case the heading is gone)
            var h1 = document.createElement('h1');
            h1.textContent = 'MarkAdminsの設定';
            var container = document.getElementById('mac-container');
            if (container) container.prepend(h1);
        }

        // Add ToC
        var node;
        if ((node = document.getElementById('mac-config-body'))) node.prepend(toc);

        // Create multi-select
        // @ts-ignore "Cannot find name 'OO'."
        var widget = new OO.ui.MenuTagMultiselectWidget({
            inputPosition: 'outline',
            options: cfg.runOnOptions,
            allowedValues: cfg.runOnOptions.map(function(obj) { return obj.data; }),
            selected: cfg.runOn
        });
        widget.$element[0].style.fontSize = '90%';
        widget.$element[0].style.width = '100%';
        widget.$element[0].style.maxWidth = 'initial';
        if ((node = document.getElementById('mac-config-runonnamespace'))) node.appendChild(widget.$element[0]);

        // Event listeners

        /**
         * Highlight default value notations when settings are changed to a non-default value.
         * Note that every /<tr> that has child elements to change settings should have custom data attributes
         * storing the default value. Use caution when you need to add new config options. For this to work,
         * the type attribute for \<input> must always be set, even for a mere textbox (don't forget "type=text"
         * for these).
         */
        var highlightNonDefaults = function() {
            if ($(this).hasClass('oo-ui-inputWidget-input')) return;
            var $tr = $(this).closest('tr');
            var inputType = $(this).attr('type'); // \<input>s should always have a type attribute, even when it's just a textbox
            var defaultValue, $defaultValueSpan;
            if (inputType === 'checkbox') {
                defaultValue = $tr.attr('data-enabled') === 'true';
                $defaultValueSpan = $tr.find('.mac-config-defaultenabled').eq(0);
                $defaultValueSpan.css({color: this.checked !== defaultValue ? 'red' : ''});
            } else if (inputType === 'text') {
                defaultValue = $tr.attr('data-label');
                $defaultValueSpan = $tr.find('.mac-config-defaultlabel').eq(0);
                $defaultValueSpan.css({color: this.value !== defaultValue ? 'red' : ''});
            }
        };
        $('#mac-config-body table input[type=text]').on('input', highlightNonDefaults);
        $('#mac-config-body table input[type=checkbox]').on('change', highlightNonDefaults);

        // Restore the default settings for a given row
        $('.mac-config-reset-this').on('click', function() {
            var $tr = $(this).closest('tr');
            $tr.find('input[type=text]').not('.oo-ui-inputWidget-input').eq(0).val($tr.attr('data-label') || 'エラー').trigger('input');
            $tr.find('input[type=checkbox]').eq(0).prop('checked', $tr.attr('data-enabled') === 'true').trigger('change');
        });

        // Restore the default settings for all the user groups
        $('#mac-config-usergroup-resetall').on('click', function() {
            $('tr.mac-config-usergroup').each(function() {
                $(this).find('.mac-config-reset-this').trigger('click');
            });
        });

        // Restore the default settings for the namespaces to run the script on
        $('#mac-config-runonnamespace-reset').on('click', function() {
            widget.setValue(MarkAdmins.defaults.runOn);
        });

        // Restore the default settings for all the target page options
        $('#mac-config-targetpage-resetall').on('click', function() {
            $('#mac-config-runonnamespace-reset').trigger('click');
            $('tr.mac-config-targetpage').each(function() {
                $(this).find('.mac-config-reset-this').trigger('click');
            });
        });

        // Save config
        $('#mac-config-save').on('click', function() {
            MarkAdmins.saveConfig(widget);
        });

        // Restore the default settings for all the options
        $('#mac-config-resetall-do').on('click', function() {
            $('.mac-config-resetall').each(function() {
                $(this).trigger('click');
            });
        });

        // Initialization when the page is loaded

        // Initialize default value highlighters
        $('#mac-config-body table input').not('[type=button]').each(function() {
            var eventType = $(this).attr('type') === 'text' ? 'input' : 'change';
            $(this).trigger(eventType);
        });

    },

    saveConfig: function(widget) {

        var newCfg = {};

        // Get user group configs
        newCfg.groups = {};
        var labels = [], duplicateLabels = [];
        $('.mac-config-usergroup').each(function() {
            var group = $(this).attr('data-group');
            var label = $(this).find('.mac-config-grouplabel').val();
            var enabled = $(this).find('.mac-config-groupenabled').prop('checked');
            newCfg.groups[group] = {
                label: label,
                enabled: enabled
            };
            if (labels.indexOf(label) === -1) {
                labels.push(label);
            } else {
                if (duplicateLabels.indexOf(label) === -1) {
                    duplicateLabels.push(label);
                }
            }
        });

        // Warn if some labels are duplicates
        if (duplicateLabels.length !== 0) {
            var msg = '重複したラベルがあります\n\n' + duplicateLabels.join('\n') + '\n\nこのまま保存しますか?';
            if (!confirm(msg)) return;
        }

        // Get namespace config
        newCfg.runOn = widget.getValue();

        // Get boolean configs
        var booleanCfg = ['runOnHistory', 'runOnTalk', 'runOnDiff', 'markSubpages', 'markMyself', 'markContribs'];
        booleanCfg.forEach(function(cfgName) {
            var $el = $('#mac-config-' + cfgName);
            newCfg[cfgName] = $el.prop('checked');
        });

        // Stringify the config object
        var newCfgStr = JSON.stringify(newCfg);

        // API call to save the config
        var $saving = $('#mac-config-saving');
        var $savingTd = $saving.children('td').eq(0);
        var $lastsaved = $('#mac-config-lastsaved');
        var $lastsavedTd = $lastsaved.children('td').eq(0);
        var $toDisable = $('#mac-config-body input');
        var msgTimeout;

        clearTimeout(msgTimeout); // Prevent saving message overwriting
        $saving.css('display', 'block');
        $savingTd.prop('innerHTML', '保存しています ' + MarkAdmins.images.loading);
        $toDisable.prop('disabled', true);
        widget.setDisabled(true);

        new mw.Api().saveOption(MarkAdmins.userOptionName, newCfgStr)
            .then(function() { // Success

                $savingTd.prop('innerHTML', '保存しました ' + MarkAdmins.images.check);
                $lastsaved.css('display', 'block');
                $lastsavedTd.text('最終保存: ' + new Date().toJSON().split('.')[0]);
                mw.user.options.set(MarkAdmins.userOptionName, newCfgStr);

            }).catch(function(code, err) { // Failure

                mw.log.error(err.error.info);
                $savingTd.prop('innerHTML', '保存に失敗しました ' + MarkAdmins.images.cross);

            }).then(function() {

                $toDisable.prop('disabled', false);
                widget.setDisabled(false);
                msgTimeout = setTimeout(function() { // Hide the progress message after 5 seconds
                    $saving.css('display', 'none');
                    $savingTd.empty();
                }, 5000);

            });

    },

    createPortletLink: function() {
        mw.util.addPortletLink(
            'p-tb',
            '/wiki/Special:MarkAdminsConfig',
            'MarkAdminsの設定',
            't-mac',
            'MarkAdminsの設定を変更する'
        );
    },

    /**
     * Get all aliases associated with a certain namespace number
     * @param {number} nsNum
     * @returns {Array<string>} Lowercase, underscores are replaced by spaces
     * @static
     */
    getNamespaceAliases: function(nsNum) {
        var nsIds = mw.config.get('wgNamespaceIds');
        return Object.keys(nsIds)
            .filter(function(alias) {
                return nsIds[alias] === nsNum;
            })
            .map(function(alias) {
                return alias.replace(/_/g, ' ');
            });
    },

    fullPageProcessed: false,

    markUser: function($content) {

        var cfg = MarkAdmins.config;

        // Filter enabled groups (Do it here and not later on each anchor)
        /**
         * @type {Object.<string, string>}
         * @key enabled group name
         * @value marker e.g. A
         */
        var enabledGroups = Object.keys(cfg.groups)
            .filter(function(gName) {
                return cfg.groups[gName].enabled;
            })
            .reduce(function(acc, gName) {
                acc[gName] = cfg.groups[gName].label;
                return acc;
            }, Object.create(null));

        if (!MarkAdmins.fullPageProcessed) $content = mw.util.$content || $content;
        if (!$content[0]) return;

        /** @type {HTMLCollectionOf<HTMLAnchorElement>} */
        var rawAnchors = $content[0].getElementsByTagName('a');
        /** @type {Array<HTMLAnchorElement>} */
        var anchors = Array.prototype.slice.call(rawAnchors);

        // Add also the userpage link
        var isUserpage = [2, 3].indexOf(MarkAdmins.nsNumber) !== -1;
        var nstab;
        if (isUserpage && !MarkAdmins.fullPageProcessed &&
            (nstab = document.getElementById('ca-nstab-user')) &&
            (nstab = nstab.getElementsByTagName('a'))
        ) {
            anchors.push(nstab[0]);
        }
        MarkAdmins.fullPageProcessed = true;

        /**
         * @type {Object.<string, string>}
         * @key username
         * @value marker e.g. (A)
         */
        var marker = {};
        if (!cfg.markMyself) marker[mw.config.get('wgUserName')] = ''; // marker: { "YourUserName": "" }

        // Create regex
        var regex = {
            article: new RegExp(mw.config.get('wgArticlePath').replace('$1', '([^?]+)')), // /wiki/PAGENAME (query parameters removed)
            script: new RegExp('^' + mw.config.get('wgScript') + '\\?title=([^&]+)'), // ^/w/index.php?title=PAGENAME (internal links only)
            user: new RegExp('^(?:' + MarkAdmins.getNamespaceAliases(2).join('|') + '):(.+)', 'i'),
            usertalk: new RegExp('^(?:' + MarkAdmins.getNamespaceAliases(3).join('|') + '):(.+)', 'i'),
            contribs: new RegExp('^(?:' + MarkAdmins.getNamespaceAliases(-1).join('|') + '):(?:投稿記録|contrib(?:ution)?s)\/(.+)', 'i')
        };

        /**
         * @type {Array<{username?: string, pagetype?: string}>}
         */
        var previousUsers = [];
        var noSubpages = !cfg.markSubpages || !!({ Prefixindex: 1, Allpages: 1 })[mw.config.get('wgCanonicalSpecialPageName')];

        anchors.forEach(function(a, i) {

            /*
             * Temporarily push an empty object as the user information associated with the current anchor
             * (This must be done at the very first in the loop; more precisely before any "return")
             */
            previousUsers.push({});

            // Ignore buttons
            if (a.role === 'button') return;

            // Ignore anchors embedded in image
            var ch;
            if ((ch = a.children[0]) && ch.nodeName === 'IMG') return;

            // Ignore section links in edit summary
            var pr;
            if ((pr = a.parentElement) && pr.classList.contains('autocomment')) return;

            // Get href
            var href = a.getAttribute('href');
            if (!href) {
                if (isUserpage && a.classList.contains('mw-selflink')) {
                    href = '/wiki/' + mw.config.get('wgPageName');
                } else {
                    return;
                }
            }

            /*
             * It's quite complicated to deal with index.php links. They are used everywhere for meta links like date links on
             * history, undo links, and a lot more. This can be a serious issue for this script because, when we're on an admin's
             * usertalk history for instance, almost every link has a "index.php?title=User talk:ADMIN&..." href, which means
             * all of these will be marked up. However, we do want to look at index.php links because this script also marks up
             * contribs links and they can be preceded by user/usertalk links like "user -> usertalk -> contribs", where we want to
             * ONLY mark up the first, and user/usertalk links can be redlinks, which are generated oftentimes by index.php.
             * Just looking also at index.php links will over-mark, so let's look only at redlinks for index.php. (This automatically
             * means that index.php contribs links cannot be dealt with.)
             */ 
            var m;
            var isRedlink = a.classList.contains('new');
            if ((m = regex.article.exec(href)) || isRedlink && (m = regex.script.exec(href))) {
                href = m[1];
            } else {
                return;
            }
            href = decodeURIComponent(href).replace(/_/g, ' ');

            // Extract non-prefixed title ( /wiki/User talk:Foo/subpage -> Foo/subpage or /wiki/Special:Contributions/Foo -> Foo )
            var nonPrefixedTitle, pagetype;
            if ((m = regex.user.exec(href))) {
                nonPrefixedTitle = m[1];
                pagetype = 'user';
            } else if ((m = regex.usertalk.exec(href))) {
                nonPrefixedTitle = m[1];
                pagetype = 'usertalk';
            } else if ((m = regex.contribs.exec(href))) {
                nonPrefixedTitle = m[1];
                pagetype = 'contribs';
            } else {
                return;
            }

            // Extract user and check if this is a link to a subpage
            var user = nonPrefixedTitle.replace(/[/#].*/, '');
            var isSubpage = user !== nonPrefixedTitle;
            if (isSubpage && noSubpages) return;

            /*
             * Don't mark the current anchor if the last anchor was for the same user and of a certain type
             * Consective links are basically in the order of "user -> usertalk -> contribs" or "contribs -> usertalk".
             * We don't want to mark hierarchically-lower-level consective links.
             */
            var prev;
            /*
             * Check whether this is a consective link that follows a master link, which is the top of the hierarchy.
             * "prev" being an empty object always means that the last anchor wasn't marked; hence the current anchor can
             * be marked. If "prev" is NOT an empty object, the current anchor can be marked only if it and the last anchor
             * are for different users AND the current anchor is not one at a lower level in the hierarchy. 
             */
            if (!isSubpage && (prev = previousUsers[i - 1]) && !$.isEmptyObject(prev) && prev.username === user) {
                if (prev.pagetype === 'user' && pagetype === 'usertalk' ||
                    prev.pagetype === 'usertalk' && pagetype === 'contribs' ||
                    prev.pagetype === 'contribs' && pagetype === 'usertalk'
                ) {
                    // This anchor won't be marked but the next loop will need this anchor's information
                    previousUsers[i] = {username: user, pagetype: pagetype}; // Update
                    return;
                }
            }

            var userM = marker[user];
            if (userM === undefined) { // Marker has yet to be created

                // User groups of selected user
                var usergroups = MarkAdmins.usersData ? MarkAdmins.usersData[user] : null;
                if (!usergroups) return;

                // Create marker
                userM = usergroups
                    .filter(function(g) {
                        return typeof enabledGroups[g] !== 'undefined';
                    })
                    .map(function(g) {
                        return enabledGroups[g];
                    })
                    .join('/');
                if (userM) userM = '(' + userM + ')';
                marker[user] = userM;

            }
            if (!userM) return; // Are there markers at all?

            // Check finished, now mark the user
            a.dataset.groupmarker = marker[user];
            a.classList.add('adminMark');

            // Now that the current anchor has been marked, the "prev" object for it should not be empty since it's empty
            // only when the anchor isn't marked
            previousUsers[i] = {username: user, pagetype: pagetype};

        });

    }

};

MarkAdmins.init();

// @ts-ignore "Cannot find name 'mediaWiki'."
}(mediaWiki, jQuery));
// </nowiki>