「プロジェクト:ウィキ技術部/スクリプト開発/trunk/MassProtect.js」の版間の差分

削除された内容 追加された内容
リダイレクトページへの保護タグ添付に対応
特別:差分/90117350に対応 (保護⇒タグ添付⇒保護⇒タグ添付...の順で実行、進捗状態表示を一行に)
557行目: 557行目:
ep.pages.forEach(function(page) {
ep.pages.forEach(function(page) {
$('#mp-progress').append(
$('#mp-progress').append(
'<div class="mp-progress-details">' +
'<p>' +
`<p class="mp-progress-page">${page}</p>` +
`<span class="mp-progress-page" style="font-weight: bold;">${page}</span> ` +
'<ul>' +
`保護: <span class="mp-progress-protect">${spinner}</span> ` +
'<li>' +
(ep.tag ? `タグ: <span class="mp-progress-tag">${spinner}</span>` : '') +
'</p>'
`保護: <span class="mp-progress-protect">${spinner}</span>` +
'</li>' +
(ep.tag ? // タグ添付をしない時はタグ添付の進捗状況は表示しない
'<li>' +
`タグ: <span class="mp-progress-tag">${spinner}</span>` +
'</li>' :
''
) +
'</ul>' +
'</div>'
);
);
});
});
var scrollDown = function(pos) {
$('#mp-progress').scrollIntoView();
var i = window.scrollY;
var int = setInterval(function() {
window.scrollTo(0, i);
i += 20;
if (i >= pos) clearInterval(int);
}, 10);
};
scrollDown($('#mp-progress').prop('offsetTop'));


// 保護を実行
// 保護を実行
MassProtect.template = { // ウィキテキストからテンプレートを抽出する際に使用
var protected = []; // 保護に成功したページ名とそのページ内容などを格納 // protected = [
'names': ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // この名前のテンプレートを抽出
var protectCnt = 0; // {
'prefixes': ['pp-'] // この文字から始まるテンプレートを抽出
// "title": pagetitle,
};
// "content": pagecontent,
var protectCnt = 0;
// "basetimestamp": latestRevisionTimestamp
protectPages();
// },

// {...}
/**
// ]
var protectPages = function() { // ページの保護を実行する関数 (意図的にコールバックで順番に実行)
* ページの保護を実行する関数 (意図的にコールバックで順番に実行)
*/
function protectPages() {
var pagetitle = ep.pages[protectCnt];
var pagetitle = ep.pages[protectCnt];
var params = {
var params = {
605行目: 606行目:
} else { // 成功
} else { // 成功
updateProgress(pagetitle, 'protect', true);
updateProgress(pagetitle, 'protect', true);
if (ep.tag) protected.push({'title': pagetitle}); // 保護タグの添付が必要ならページ名を保存
}
}


protectCnt++;
protectCnt++;
if (ep.pages[protectCnt]) {
if (ep.tag) {
protectPages();
pasteTag(pagetitle);
} else {
} else {
if (ep.pages[protectCnt]) protectPages();
if (ep.tag) pasteTag(); // 保護タグの添付をする場合、全てのページの保護が完了したら次のプロセスに移行
}
}


});
});
};
}


function pasteTag(pagetitle) {
MassProtect.template = { // ウィキテキストからテンプレートを抽出する際に使用
'names': ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // この名前のテンプレートを抽出
'prefixes': ['pp-'] // この文字から始まるテンプレートを抽出
};


if (pagetitle.match(/^(?:モジュール|[Mm]odule):/)) { // タグ添付対象がモジュール名前空間のページなら
/**
updateProgress(pagetitle, 'tag', 'モジュール', true); // 進捗を「中止 (モジュール)」にして
* 保護処理のループ終了後に実行する保護タグ添付用関数
if (ep.pages[protectCnt]) protectPages(); // タグ添付をせずに次の保護処理に移行
*/
return;
var pasteTag = function() {
}

var protectedPages = protected.filter(function(obj) { // ページタイトルの配列を作成 (対象となるのは保護に成功したページ群のみ)
if (obj.title.match(/^(?:モジュール|[Mm]odule):/)) {
updateProgress(obj.title, 'tag', 'モジュール', true);
}
return !obj.title.match(/^(?:モジュール|[Mm]odule):/); // モジュール名前空間にはタグを添付できないため配列から除去
}).map(function(obj) {
return obj.title;
});


// タグ添付対象ページの最新版を取得
// タグ添付対象ページの最新版を取得
$.get(mw.util.wikiScript('api'), {
$.get(mw.util.wikiScript('api'), {
'action': 'query',
'action': 'query',
'titles': protectedPages.join('|'), // 500要素以上だとエラーが返ってくるため注意
'titles': pagetitle,
'prop': 'info|revisions',
'prop': 'info|revisions',
'rvprop': 'content',
'rvprop': 'content',
649行目: 638行目:


var resPages;
var resPages;
if (!res || !res.query || !(resPages = res.query.pages)) return resolveAllProgress();
if (!res || !res.query || !(resPages = res.query.pages)) {
updateProgress(pagetitle, 'tag', '通信エラー', true);
if (ep.pages[protectCnt]) protectPages();
return;
}

var curtimestamp = res.curtimestamp;
var curtimestamp = res.curtimestamp;
for (var key in resPages) { // キーはランダムナンバー (ページID)
for (var key in resPages) { // キーはランダムナンバー (ページID)
(function() { // 一応即時関数でforのブロックスコープを作成


var resObj = resPages[key];
var resObj = resPages[key];
var pageMissing = resObj.missing === '';
var pageMissing = resObj.missing === '';
var isRedirect = resObj.redirect === '';
var isRedirect = resObj.redirect === '';


// タグ添付しないページ群の剪定
// ページが未作成の場合はタグ添付しない
if (pageMissing) { // ページが未作成なら
if (pageMissing) {
protected = protected.filter(function(obj) {
updateProgress(pagetitle, 'tag', '未作成', true);
if (ep.pages[protectCnt]) protectPages();
return obj.title !== resObj.title; // タグ添付不要のため protected 配列から該当要素を除去
});
return;
}
return updateProgress(resObj.title, 'tag', '未作成', true);
}


// タグ添付するページの編集に必要な情報を取得
// タグ添付するページの編集に必要な情報を取得
var basetimestamp = resObj.touched;
protected.every(function(obj) { // array.every() はtrueが返った場合ループを継続し、falseが返った場合は終了する
if (obj.title === resObj.title) {
var content = resObj.revisions[0].slots.main['*']; // ページ内容を取得
var originalContent = JSON.parse(JSON.stringify(content)); // 後で比較する用の魚拓
var templates = findTemplates(content, MassProtect.template.names, MassProtect.template.prefixes); // 除去対象のテンプレートを取得
if (templates.length !== 0) {
templates = templates.map(function(item) {
return mw.util.escapeRegExp(item); // 正規表現内で使用するため特殊文字をエスケープ
});
var regex = new RegExp(`(?:${templates.join('|')})[^\\S\\n\\r]*\\n?`, 'g');
content = content.replace(regex, ''); // 「{{TEMPLATE}}\n」を空の文字列に置換
}


// 最新版のタイムスタンプ
// 冒頭に保護グを挿入 (リダレクトとテンプレート名前空間のページは別処理が必要)
obj.basetimestamp = resObj.touched;
if (isRedirect) { // リダイレクトなら
content = content.trim(); // リダイレクトのリンクの前に改行があることがあるのでそれを除去
if (content.indexOf('\n') === -1) { // ページ内容が転送リンクの一行のみなら
content += '\n' + ep.tag; // 2行目にタグを挿入
} else { // 複数行あれば
content = content.replace('\n', '\n' + ep.tag + '\n'); // 1つ目の改行の後にタグを挿入
}
} else {
if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) { // .cssページなら
content = '/*' + ep.tag + '*/' + '\n' + content; // コメントアウト
} else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/)) { // .cssページでなければ
content = '<noinclude>' + ep.tag + '</noinclude>' + content; // トランスクルードさせない
} else {
content = ep.tag + '\n' + content;
}
}


}
// ページ内容を取得し、除去が必要なテンプレートは置換
var content = resObj.revisions[0].slots.main['*'];
var templates = findTemplates(content, MassProtect.template.names, MassProtect.template.prefixes); // 除去対象のテンプレートを取得
if (templates.length !== 0) {
templates = templates.map(function(item) {
return mw.util.escapeRegExp(item); // 正規表現内で使用するため特殊文字をエスケープ
});
var regex = new RegExp(`(?:${templates.join('|')})[^\\S\\n\\r]*\\n?`, 'g');
content = content.replace(regex, ''); // 「{{TEMPLATE}}\n」を空の文字列に置換
}

// 冒頭に保護タグを挿入 (リダイレクトとテンプレート名前空間のページは別処理が必要)
if (isRedirect) { // リダイレクトなら
content = content.replace(/\u200e/g, '').trim(); // バグの原因となりうる赤点スペースを全除去しtrim
if (content.indexOf('\n') === -1) { // ページ内容が転送リンクの一行のみなら
content += '\n' + ep.tag; // 2行目にタグを挿入
} else { // 複数行あれば
content = content.replace('\n', '\n' + ep.tag + '\n'); // 1つ目の改行の後にタグを挿入
}
} else {
if (obj.title.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) { // .cssページなら
content = '/*' + ep.tag + '*/' + '\n' + content; // コメントアウト
} else if (obj.title.match(/^(?:[Tt]emplate|テンプレート):/)) { // .cssページでなければ
content = '<noinclude>' + ep.tag + '</noinclude>' + content; // トランスクルードさせない
} else {
content = ep.tag + '\n' + content;
}
}
obj.content = content;


// 編集前と編集後で同じ内容になる場合は編集しない
return false; // 値を取得し終わったらループを止める
if (content === originalContent) {
} else {
updateProgress(pagetitle, 'tag', '同じ内容', true);
if (ep.pages[protectCnt]) protectPages();
return true; // 値が取得できるまでループを継続
}
return;
});

})();
}
}


// ページの編集
// ページの編集
protected.forEach(function(obj) {
$.post(mw.util.wikiScript('api'), {
$.post(mw.util.wikiScript('api'), {
'action': 'edit',
'action': 'edit',
'title': pagetitle,
'title': obj.title,
'text': content,
'text': obj.content,
'summary': ep.tag + ' (MassProtect)',
'summary': ep.tag + ' (MassProtect)',
'minor': 1,
'minor': 1,
'basetimestamp': basetimestamp,
'basetimestamp': obj.basetimestamp,
'starttimestamp': curtimestamp,
'starttimestamp': curtimestamp,
'nocreate': 1,
'nocreate': 1,
'watchlist': 'nochange',
'watchlist': 'nochange',
'token': mw.user.tokens.get('csrfToken'),
'token': mw.user.tokens.get('csrfToken'),
'format': 'json'
'format': 'json'
}, function(res2) {

}, function(res) {
if (res.error) mw.log.error(obj.title + ': ' + res.error.info);
if (res2.error) { // 何らかの理由で編集に失敗
return updateProgress(obj.title, 'tag', true);
updateProgress(pagetitle, 'tag', res2.error.info);
});
} else if (!res2 || !res2.edit) { // 不明なエラー
updateProgress(pagetitle, 'tag', false);
} else {
if (res2.edit.result === 'Success') { // 成功
updateProgress(pagetitle, 'tag', true);
} else {
updateProgress(pagetitle, 'tag', res2.edit.result);
}
}

if (ep.pages[protectCnt]) protectPages();

});
});


});
});
};
}

protectPages();


});
});
749行目: 743行目:
* @param {number} page 保護対象ページ名+
* @param {number} page 保護対象ページ名+
* @param {string} type 'protect' または 'tag'
* @param {string} type 'protect' または 'tag'
* @param {boolean|string} result 保護が成功したらtrue、不明なエラーが発生したらfalse, 保護に失敗したらエラーの詳細
* @param {boolean|string} result 成功したらtrue、不明なエラーが発生したらfalse、理由の分かるエラーが発生したらその詳細
* (cancelledがtrueの場合は「中止 (XXX)」のXXXにあたる文字列)
* (cancelledがtrueの場合は「中止 (XXX)」のXXXにあたる文字列)
* @param {boolean} [cancelled]
* @param {boolean} [cancelled]
759行目: 753行目:
'<fieldset id="mp-progress">' +
'<fieldset id="mp-progress">' +
'<legend>進捗</legend>' +
'<legend>進捗</legend>' +
'<div class="mp-progress-details">' +
'<p>' +
`<p class="mp-progress-page">${page}</p>` +
`<span class="mp-progress-page">${page}</span> ` +
'<ul>' +
`保護: <span class="mp-progress-protect">${spinner}</span> ` +
'<li>' +
`タグ: <span class="mp-progress-tag">${spinner}</span>` +
'</p>' +
`保護: <span class="mp-progress-protect">${spinner}</span>` +
'</li>' +
'<li>' +
`タグ: <span class="mp-progress-tag">${spinner}</span>` +
'</li>' +
'</ul>' +
'</div>' +
'</fieldset>'
'</fieldset>'
\********************************************************************************************/
\********************************************************************************************/


var $progress = $('.mp-progress-page').filter(function() { // mp-progress-page のクラス属性を持っている <p> タグを
var $progress = $('.mp-progress-page').filter(function() { // mp-progress-page のクラス属性を持っている <span> タグを
return $(this).text() === page; // 特定のページ名を text として持つものに絞り込み
return $(this).text() === page; // 特定のページ名を text として持つものに絞り込み
}).siblings('ul').find(`.mp-progress-${type}`); // 姉妹関係にある <ul> 内の <span> が更新対象のセレクタ
}).siblings(`.mp-progress-${type}`); // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ


switch (result) {
switch (result) {
791行目: 779行目:
}
}
}
}
}

/**
* 保護成功後、タグ添付前の最新版取得に失敗した際に全ての進捗情報を「失敗 (通信エラー)」にする
*/
function resolveAllProgress() {
var displayText = '<span class="mp-resolved" style="color: MediumVioletRed;">失敗 (通信エラー)</span>';
$('#mp-progress').find('span').filter(function() {
return !$(this).hasClass('mp-resolved'); // まだスピナーが表示されている <span> に絞り込み
}).each(function() { // その全ての <span> を
$(this).prop('outerHTML', displayText); // displayTextのメッセージに置換
});
}
}



2022年6月20日 (月) 18:10時点における版

/*********************************************************************************************\
    MassProtect (一括保護スクリプト)
    管理者権限を所有する利用者が以下のいずれかの特別ページにアクセスすることで使用できます。
    * [[特別:一括保護]]
    * [[特別:MassProtect]] (※ オリジナル版と競合しないように暫定的に無効化中)
    * [[特別:MP]]
      (※ 'Special:' でも可)
    作者: (オリジナル) [[en:User:Timotheus Canens]]
                      => [[en:User:Timotheus Canens/massprotect.js]]
         (移入)      [[User:Infinite0694]]
                      => [[User:Infinite0694/Mass protecting tool ja.js]]
          (大幅改変)  [[User:Dragoniez]] 
                      => このスクリプト
                      (編集者: )
    このスクリプトを使用すると大量のページを一瞬で保護することが可能になりますが、基本的に使用者の
    責任で使用してください。
\*********************************************************************************************/
//<nowiki>

(function(mw, $) {

// ******************* ページの初期化 ***********************

// 関数間で共有する変数を格納するためのオブジェクト
var MassProtect = {};

// フォーム生成 (該当特別ページ以外の場合はポートレットリンクを生成)
// if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm]ass[Pp]rotect|[Mm][Pp])$/)) {
if (mw.config.get('wgPageName').match(/^(?:特別|Special):(?:一括保護|[Mm][Pp])$/)) {
    $(document).prop('title', '一括保護 - Wikipedia');
    if (/sysop/.test(mw.config.get('wgUserGroups'))) {
        createForm();
    } else {
        showUserRightError();
        return;
    }
} else {
    mw.util.addPortletLink('p-tb', '/wiki/特別:一括保護', '一括保護 (改)' , 't-mp', 'ページを一括で保護する');
    return;
}


// ******************* 主要関数 ***********************

function createForm() {

    var getUrl = function(pagename, params) {
        return mw.util.getUrl(pagename, params ? params : {});
    };

    var protectionLevels =
        '<option selected value="">変更なし</option>' +
        '<option value="all">すべての利用者に許可</option>' +
        '<option value="autoconfirmed">自動承認された利用者のみ許可</option>' +
        '<option value="extendedconfirmed">拡張承認された利用者と管理者に許可</option>' +
        '<option value="sysop">管理者のみ許可</option>';

    $(mw.util.$content).prop('innerHTML',
        '<div id="mp-container" style="font-size: 95%;">' +
            '<div id="mp-header">' +
                '<h1>一括保護</h1>' +
                '<p>' + 
                    '指定されたページ群の保護レベルを一括変更できます。変更する場合は、' + 
                    `<a href="${getUrl('Wikipedia:保護の方針')}" title="Wikipedia:保護の方針">保護の方針</a>、` +
                    `<a href="${getUrl('Wikipedia:拡張半保護の方針')}" title="Wikipedia:拡張半保護の方針">拡張半保護の方針</a>、` +
                    `<a href="${getUrl('Wikipedia:半保護の方針')}" title="Wikipedia:半保護の方針">半保護の方針</a>` +
                    'に基づいているか確認して下さい。' +
                '</p>' +
                '<ul>' +
                    '<li>有効期限のデフォルトは無期限です。適切な期間・期限を指定してください。</li>' +
                    '<ul>' +
                        '<li>' +
                            '「その他の期間」の記入例 (' +
                            `<a href="http://www.gnu.org/software/tar/manual/html_node/Date-input-formats.html">GNU標準フォーマット</a>` +
                            '): "12 hours"、"5 days"、"3 weeks"、"2012-09-25 20:00"' +
                            ` (日時は<a href="${getUrl('協定世界時')}" title="協定世界時">UTC</a>)` +
                        '</li>' +
                    '</ul>' +
                    '<li>' +
                        '保護レベルを変更した場合、ページ上で保護テンプレート (' +
                        `<a href="${getUrl('Template:Pp')}">Template:Pp</a>` +
                        ') を更新してください。' +
                    '</li>' +
                '</ul>' +
            '</div>' +
            '<div id="mp-body">' +
                '<form>' +
                    '<fieldset id="mp-protection-settings">' +
                        '<legend>一括保護</legend>' +
                        '<fieldset>' +
                            '<legend>保護対象ページ</legend>' +
                            '<textarea id="mp-targetpages" placeholder="ページごとに改行して記入" rows="15" style="font-family: inherit;"></textarea>' +
                            '<input id="mp-targetpages-cleanup" type="button" value="整形" style="margin: 0.5em 0.5em 0 0;">' +
                            '<label for="mp-targetpages-cleanup">(重複分および余分な改行を除去)</label><br>' +
                            '<p>ページ数: <span id="mp-targetpages-count">0</span></p>' +
                            '<div id="mp-targetpages-search" style="margin-top: 1em;">' +
                                '<div id="mp-targetpages-search-input-div" style="margin: 0;">' +
                                    'ページ情報検索<br>' +
                                    '<input id="mp-targetpages-search-input" placeholder="ページ名" style="width: 30ch; margin-right: 0.5em;">' +
                                    '<p id="mp-targetpages-search-links" style="display: none; margin: 0;">' +
                                        ' (<a id="mp-targetpages-search-history" href="">履歴</a> /' +
                                        ' <a id="mp-targetpages-search-log" href="">記録</a> /' +
                                        ' <a id="mp-targetpages-search-log" href="">保護記録</a>)' +
                                    '</p>' +
                                '</div>' +
                                '<div id="mp-targetpages-search-status-div" style="display: none; margin: 0;">' +
                                    '<p id="mp-targetpages-search-status" style="display: inline-block; margin: 0.2em 0 0 0;"></p>' +
                                '</div>' +
                                '<div id="mp-targetpages-search-missing-div" style="display: none; color: MediumVioletRed; margin: 0;">' +
                                    '<p id="mp-targetpages-search-missing" style="display: inline-block; margin: 0;">未作成のページ</p>' +
                                '</div>' +
                            '</div>' +
                        '</fieldset>' +
                        '<fieldset>' +
                            '<legend>保護レベル</legend>' +
                            '<label for="mp-protectionlevel-create" style="width: 8ch; display: inline-block;">作成保護</label>' +
                            '<select class="mp-protectionlevel" id="mp-protectionlevel-create" style="margin-bottom: 0.3em;">' +
                                protectionLevels +
                            '</select><br>' +
                            '<label for="mp-protectionlevel-edit" style="width: 8ch; display: inline-block;">編集保護</label>' +
                            '<select class="mp-protectionlevel" id="mp-protectionlevel-edit" style="margin-bottom: 0.3em;">' +
                                protectionLevels +
                            '</select><br>' +
                            '<label for="mp-protectionlevel-move" style="width: 8ch; display: inline-block;">移動保護</label>' +
                            '<select class="mp-protectionlevel" id="mp-protectionlevel-move" style="margin-bottom: 0.3em;">' +
                                protectionLevels +
                            '</select><br>' +
                            '<input id="mp-watchlist" type="checkbox" style="margin-right: 0.3em;">' +
                            '<label for="mp-watchlist">保護対象ページをウォッチリストに追加</label><br>' +
                            '<input id="mp-cascade" type="checkbox" style="margin-right: 0.3em;">' +
                            '<label for="mp-cascade">カスケード保護 (保護対象ページで読み込まれているテンプレートと画像の自動保護)</label><br>' +
                            '<input id="mp-tag" type="checkbox" style="margin-right: 0.3em; margin-bottom: 0.3em;">' +
                            '<label for="mp-tag">' +
                                '保護タグを自動添付' +
                                ` (<a href="${getUrl('Template:Pp#関連項目')}" title="Template:Pp#関連項目">一覧</a>)` +
                            '</label>' +
                            '<ul id="mp-tag-list" style="display: none;">' +
                                '<li>' +
                                    '<label for="mp-tag-template" style="width: 12ch; display: inline-block;">テンプレート</label>' +
                                    '<select id="mp-tag-template" style="width: 20ch; margin-bottom: 0.3em;">' +
                                        '<option id="mp-tag-template-meta" value="pp">{{pp}}</option>' +
                                        '<option id="mp-tag-template-dispute" value="pp-dispute">{{pp-dispute}}</option>' +
                                        '<option selected id="mp-tag-template-vandalism" value="pp-vandalism">{{pp-vandalism}}</option>' +
                                        '<option id="mp-tag-template-template" value="pp-template">{{pp-template}}</option>' +
                                        '<option id="mp-tag-template-permanent" value="pp-permanent">{{pp-permanent}}</option>' +
                                        '<option id="mp-tag-template-move" value="pp-move">{{pp-move}}</option>' +
                                        '<option id="mp-tag-template-move-vandalism" value="pp-move-vandalism">{{pp-move-vandalism}}</option>' +
                                        '<option id="mp-tag-template-move-dispute" value="pp-move-dispute">{{pp-move-dispute}}</option>' +
                                        '<option id="mp-tag-template-office" value="pp-office">{{pp-office}}</option>' +
                                        '<option id="mp-tag-template-reset" value="pp-reset">{{pp-reset}}</option>' +
                                        '<option id="mp-tag-template-office-dmca" value="pp-office-dmca">{{pp-office-dmca}}</option>' +
                                    '</select>' +
                                '</li>' +
                                '<li>' +
                                    '<label for="mp-tag-action" style="width: 12ch; display: inline-block;">種別</label>' +
                                    '<select id="mp-tag-action" style="width: 20ch; margin-bottom: 0.3em;">' +
                                        '<option selected id="mp-tag-action-blank" value=""></option>' +
                                        '<option id="mp-tag-action-edit" value="">edit</option>' +
                                        '<option id="mp-tag-action-move" value="|action=move">move</option>' +
                                        '<option id="mp-tag-action-upload" value="|action=upload">upload</option>' +
                                    '</select>' +
                                '</li>' +
                                '<li>' +
                                    '<label for="mp-tag-small" style="width: 12ch; display: inline-block;">縮小表示</label>' +
                                    '<select id="mp-tag-small" style="width: 20ch; margin-bottom: 0.3em;">' +
                                        '<option id="mp-tag-small-blank" value=""></option>' +
                                        '<option id="mp-tag-small-yes" value="|small=yes">small=yes</option>' +
                                        '<option hidden id="mp-tag-small-no" value="|small=no">small=no</option>' +
                                    '</select>' +
                                '</li>' +
                                '(既存の保護タグ・保護依頼タグは自動除去され、リダイレクトページには添付されません)' +
                            '</ul>' +
                        '</fieldset>' +
                        '<label for="mp-reason-dropdown" style="width: 6ch; display: inline-block;">理由</label>' +
                        '<select id="mp-reason-dropdown" style="width: auto; margin-bottom: 0.3em;">' +
                            '<optgroup label="その他の理由">' +
                                '<option value="other">その他の理由</option>' +
                            '</optgroup>' +
                            '<optgroup label="保護理由">' +
                                '<option>度重なる荒らし</option>' +
                                '<option>度重なる宣伝</option>' +
                                '<option>編集合戦</option>' +
                                '<option>移動合戦</option>' +
                                '<option>プライバシー侵害の記述の繰り返し</option>' +
                                '<option>IP利用者による問題投稿の繰り返し</option>' +
                                '<option>IP・新規利用者による問題投稿の繰り返し</option>' +
                                '<option>[[WP:SOCK|sockpuppet]]による問題投稿の繰り返し</option>' +
                                '<option>[[Wikipedia:保護依頼]]による</option>' +
                                '<option>削除タグ剥離</option>' +
                                '<option>移動不要ページ</option>' +
                                '<option>利用者ページ 本人希望</option>' +
                                '<option>削除されたページの改善なき再作成の繰り返し</option>' +
                                '<option>更新の必要性が低い重要ページ</option>' +
                                '<option>[[WP:HRT|影響が特に大きいテンプレート]]</option>' +
                                '<option>議論が終了済みの過去ログ</option>' +
                                '<option>履歴保存</option>' +
                                '<option>特定版削除に伴う保護のかけなおし</option>' +
                                '<option>特定版削除に伴う半保護のかけなおし</option>' +
                            '</optgroup>' +
                            '<optgroup label="解除理由">' +
                                '<option>[[Wikipedia:保護解除依頼]]による</option>' +
                                '<option>保護理由消滅のため</option>' +
                                '<option>編集内容の合意の成立</option>' +
                            '</optgroup>' +
                        '</select><br>' +
                        '<label for="mp-reason-custom" style="width: 6ch; display: inline-block;"></label>' +
                        '<input id="mp-reason-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
                        '<label for="mp-expiry-dropdown" style="width: 6ch; display: inline-block;">期間</label>' +
                        '<select id="mp-expiry-dropdown" style="width: 20ch; margin-bottom: 0.3em;">' +
                            '<optgroup label="デフォルトタイム">' +
                                '<option value="indefinite">無期限</option>' +
                            '</optgroup>' +
                            '<optgroup label="プリセットタイム">' +
                                '<option value="1 hour">1時間</option>' +
                                '<option value="2 hours">2時間</option>' +
                                '<option value="1 day">1日</option>' +
                                '<option value="31 hours">31時間</option>' +
                                '<option value="2 days">2日</option>' +
                                '<option value="3 days">3日</option>' +
                                '<option value="1 week">1週間</option>' +
                                '<option value="2 weeks">2週間</option>' +
                                '<option value="1 month">1ヵ月</option>' +
                                '<option value="3 months">3ヵ月</option>' +
                                '<option value="6 months">6ヵ月</option>' +
                                '<option value="1 year">1年</option>' +
                                '<option value="2 years">2年</option>' +
                                '<option value="3 years">3年</option>' +
                                '<option value="">その他の期間</option>' +
                            '</optgroup>' +
                        '</select><br>' +
                        '<label for="mp-expiry-custom" style="width: 6ch; display: inline-block;"></label>' +
                        '<input disabled id="mp-expiry-custom" style="width: 60ch; margin-bottom: 0.3em;"><br>' +
                        '<input type="button" id="mp-submit" value="保護を実行" style="margin-top: 0.5em;">' +
                    '</fieldset>' +
                '</form>' +
            '</div>' +
        '</div>'
    );

}

function showUserRightError() {
    $(mw.util.$content).prop('innerHTML',
        '<div id="mp-container" style="font-size: 95%;">' +
            '<h1>権限エラー</h1>' +
            '<p>あなたには「保護設定の変更」を行う権限がありません。理由は以下の通りです:</p>' +
            '<p>' +
                'この操作は、以下のグループに属する利用者のみが実行できます: ' +
                `<a href="${mw.util.getUrl('Wikipedia:管理者')}">管理者</a>。` +
            '</p>' +
        '</div>'
    );
}

/**
 * 保護対象ページインプットボックス内の重複分、赤点スペース、余分な改行を除去・整形 + ページ一覧を取得
 * @returns {Array}
 */
function getTargetPages() {
    var $input = $('#mp-targetpages');
    var inputVal = $input.val().replace(/\u200e/g, '').trim();
    if (inputVal) {
        inputVal = inputVal.split('\n');
        inputVal = inputVal.map(function(item) {
            return item.replace(/\u200e/g, '').replace(/_/g, ' ').trim();
        });
        inputVal = inputVal.filter(function(item, index) {
            return item !== '' && inputVal.indexOf(item) === index; // 空の文字列と重複要素をsplit配列から除去
        });
        $input.val(inputVal.join('\n'));
    } else {
        inputVal = [];
    }
    $('#mp-targetpages-count').text(inputVal.length);
    return inputVal;
}

// 「整形」ボタンを押した際に保護対象ページインプットボックス内を整理
$(document).off('click', '#mp-targetpages-cleanup').on('click', '#mp-targetpages-cleanup', getTargetPages);

// 「ページ情報検索」の値が更新されたらページ情報を表示
var inputboxUpdateTimeout;
$(document).off('input', '#mp-targetpages-search-input').on('input', '#mp-targetpages-search-input', function() {

    var $input = $(this),
        $links = $(this).siblings('p'),
        $statusDiv = $('#mp-targetpages-search-status-div'),
        $status = $statusDiv.children('p'),
        $missingDiv = $('#mp-targetpages-search-missing-div');

    clearTimeout(inputboxUpdateTimeout);
    inputboxUpdateTimeout = setTimeout(function() { // タイピング終了後3.5秒で駆動

        // ページ名を取得しリンク群をアップデート
        var pagename = $input.val().replace(/\u200e/g, '').trim();
        if (!pagename) { // インプットボックスが空白化されたら
            $links.css('display', 'none');
            $statusDiv.css('display', 'none');
            $missingDiv.css('display', 'none');
            return;
        }
        $links.css('display', 'inline-block');
        $links.children('a').eq(0).attr('href', mw.util.getUrl('特別:pagehistory/' + pagename));
        $links.children('a').eq(1).attr('href', mw.util.getUrl('特別:ログ', {'page': pagename}));
        $links.children('a').eq(2).attr('href', mw.util.getUrl('特別:ログ', {'type': 'protect', 'page': pagename}));

        /**
         * クエリの返り値を日本語に変換 (e.g. '編集半保護 (YYYY-MM-DDThh:mmまで)')
         * @param {Array} arr res.query.pages.key.protection: [{type: __, level: __, expiry: __}]
         * @returns {string}
         */
        var getProtectInfo = function(arr) {
            var protectInfo = '';
            for (var i = 0; i < arr.length; i++) {
                switch (arr[i].type) {
                    case 'create':
                        protectInfo += '作成';
                        break;
                    case 'edit':
                        protectInfo += '編集';
                        break;
                    case 'move':
                        protectInfo += '移動';
                        break;
                    case 'upload':
                        protectInfo += 'アップロード';
                }
                switch (arr[i].level) {
                    case 'autoconfirmed':
                        protectInfo += '半保護';
                        break;
                    case 'extendedconfirmed':
                        protectInfo += '拡張半保護';
                        break;
                    case 'sysop':
                        protectInfo += '全保護';
                }
                protectInfo += ' (' + (arr[i].expiry === 'infinity' ? '無期限' : arr[i].expiry.substring(0, arr[i].expiry.length - 4) + 'まで') + '), ';
            }
            if (!protectInfo) protectInfo = '保護設定なし';
            return protectInfo.replace(/, $/, '');
        };

        var updateForm = function(protectInfo, pageMissing) {

            $status.text(protectInfo);
            $statusDiv.css('display', 'block');

            if (pageMissing) {
                $missingDiv.css('display', 'block');
            } else {
                $missingDiv.css('display', 'none');
            }

        };

        // ページの保護情報を取得
        $.get(mw.util.wikiScript('api'), {
            'action': 'query',
            'titles': pagename,
            'prop': 'info',
            'inprop': 'protection',
            'format': 'json'
        }, function(res) {
            var resPages, pageMissing, protectInfo;
            if (!res || !res.query || !(resPages = res.query.pages)) return;
            for (var page in resPages) { // 1ページずつのためループは1度のみ
                if (resPages[page].missing === '') pageMissing = true;
                protectInfo = getProtectInfo(resPages[page].protection); // 加算不要
            }
            updateForm(protectInfo, pageMissing);
        });

    }, 350);
});

// 「保護タグを自動添付」がチェックされたらドロップダウンを表示
$(document).off('change', '#mp-tag').on('change', '#mp-tag', function() {
    if ($(this).is(':checked')) {
        $('#mp-tag-list').css('display', 'block');
    } else {
        $('#mp-tag-list').css('display', 'none');
    }
});

// 保護タグドロップダウンのオプションコントロール
$(document).off('change', '#mp-tag-template').on('change', '#mp-tag-template', function() {

    /********************************************************\
     * template         action                  small       *
     * pp-(meta)        edit, move, upload      def=no      *
     * dispute          edit, move, upload      X           *
     * vandalism        edit, move              def=no      *
     * (semi-indef)     ----------              ---------   *
     * template         X                       def=yes     *
     * permanent        X                       def=yes     *
     * move             X                       def=no      *
     * move-vandalism   X                       def=no      *
     * move-dispute     X                       X           *
     * office           X                       X           *
     * reset            X                       X           *
     * office-dmca      X                       X           *
    \********************************************************/

    // セレクタ
    var $action = $('#mp-tag-action'),
        $actionBlank = $('#mp-tag-action-blank'),
        $actionEdit = $('#mp-tag-action-edit'), // 未使用
        $actionMove = $('#mp-tag-action-move'), // 未使用
        $actionUpload = $('#mp-tag-action-upload'),
        $small = $('#mp-tag-small'),
        $smallBlank = $('#mp-tag-small-blank'),
        $smallYes = $('#mp-tag-small-yes'),
        $smallNo = $('#mp-tag-small-no');

    // リセット (全て表示 + actionとsmallは空オプションを選択)
    $action.prop('disabled', false);
    $action.children('option').prop('hidden', false);
    $small.prop('disabled', false);
    $small.children('option').prop('hidden', false);
    $actionBlank.prop('selected', true);
    $smallBlank.prop('selected', true);

    // オプションコントロール
    switch ($(this).children('option').filter(':selected').prop('id')) {
        case 'mp-tag-template-meta':
            $smallNo.prop('hidden', true);
            break;
        case 'mp-tag-template-dispute':
            $small.prop('disabled', true);
            break;
        case 'mp-tag-template-vandalism':
            $actionUpload.prop('hidden', true);
            $smallNo.prop('hidden', true);
            break;
        case 'mp-tag-template-template':
        case 'mp-tag-template-permanent':
            $action.prop('disabled', true);
            $smallYes.prop('hidden', true);
            break;
        case 'mp-tag-template-move':
        case 'mp-tag-template-move-vandalism':
            $action.prop('disabled', true);
            $smallNo.prop('hidden', true);
            break;
        case 'mp-tag-template-move-dispute':
        case 'mp-tag-template-office':
        case 'mp-tag-template-reset':
        case 'mp-tag-template-office-dmca':
            $action.prop('disabled', true);
            $small.prop('disabled', true);
    }

});

// カスタム期間指定インプットボックスをドロップダウンの値によって有効化・無効化
$(document).off('change', '#mp-expiry-dropdown').on('change', '#mp-expiry-dropdown', function() {
    if ($(this).val() === '') { // カスタム期間
        $('#mp-expiry-custom').prop('disabled', false);
    } else { // プリセット期間
        $('#mp-expiry-custom').val('').prop('disabled', true);
    }
});

/**
 * action=protectの 'protections' パラメータを取得 (書式: action=level (e.g. edit=sysop))
 * @returns {string|boolean} 適切なパラメータならそのパラメータ、パラメータが指定されていなければfalse、不正なパラメータなら空の文字列
 */
function getProtectionLevelParam() {
    var param = [];
    $('.mp-protectionlevel').each(function(i) {
        var type = (i === 0 ? 'create=' : (i === 1 ? 'edit=' : 'move='));
        var level = $(this).children('option').filter(':selected').val();
        if (level) param.push(type + level);
    });
    if (param.length === 0) {
        return false;
    } else {
        param = param.join('|');
        if (param.indexOf('create=') !== -1 && (param.indexOf('edit=') !== -1 || param.indexOf('move=') !== -1)) {
            return '';
        } else {
            return param;
        }
    }
}

/**
 * 保護実行前の最終確認 + 必要情報の取得
 * @returns {{pages: Array, levels: string, expiry: string, reason: string, cascade: boolean, watchlist: string, tag: string}}
 */
function editPrep() {

    // 必須フィールドの入力チェック
    var targetPages = getTargetPages();
    if (targetPages.length === 0) return alert('保護対象ページが入力されていません');
    var protectionLevel = getProtectionLevelParam();
    switch (protectionLevel) {
        case false:
            return alert('保護レベルが設定されていません');
        case '':
            return alert('未作成のページに適用される作成保護と作成済みのページに適用される編集保護・移動保護は同時指定できません');
        default:
    }

    // 保護理由を取得 (「その他」かつ空白の場合警告)
    var reasonOption = $('#mp-reason-dropdown').find('option').filter(':selected').val();
    var reasonCustom = $('#mp-reason-custom').val().replace(/\u200e/g, '').trim();
    if (reasonOption === 'other' && !reasonCustom) {
        if (!confirm('「その他の理由」が選択されていますが理由が入力されていません。このまま実行しますか?')) return;
    }
    var reason = reasonOption === 'other' ? reasonCustom : reasonOption + (reasonCustom ? ': ' + reasonCustom : '');

    // 保護タグを取得
    var tag = '';
    if ($('#mp-tag').is(':checked')) {
        var tagname = $('#mp-tag-template').children('option').filter(':selected').val(),
            tagtype = $('#mp-tag-action').children('option').filter(':selected').val(),
            tagdisplay = $('#mp-tag-small').children('option').filter(':selected').val();
        tag = (tagname + tagtype + tagdisplay) ? '{{' + tagname + tagtype + tagdisplay + '}}' : '';
    }

    // 保護期間を取得
    var expiryOption = $('#mp-expiry-dropdown').find('option').filter(':selected').val();
    var expiry = expiryOption ? expiryOption : $('#mp-expiry-custom').val().replace(/\u200e/g, '').trim();

    // オブジェクトを return
    return {
        'pages': targetPages,
        'levels': protectionLevel,
        'expiry': expiry,
        'reason': reason,
        'cascade': $('#mp-cascade').prop('checked'),
        'watchlist': $('#mp-watchlist').is(':checked') ? 'watch' : 'nochange',
        'tag': tag
    };

}

// ボタンクリック時に一括保護を実行
$(document).off('click', '#mp-submit').on('click', '#mp-submit', function() {

    // 保護設定を取得
    var ep = editPrep();
    if (!ep) return;

    // ボタンを無効化
    $(this).prop('disabled', true);

    // 進捗状態を表示
    $('#mp-protection-settings').append(
        '<fieldset id="mp-progress">' +
            '<legend>進捗</legend>' +
        '</fieldset>'
    );
    var spinner = '<img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" style="vertical-align: middle; max-height: 100%; border: 0;">';
    ep.pages.forEach(function(page) {
        $('#mp-progress').append(
            '<p>' +
                `<span class="mp-progress-page" style="font-weight: bold;">${page}</span> ` +
                `保護: <span class="mp-progress-protect">${spinner}</span> ` +
                (ep.tag ? `タグ: <span class="mp-progress-tag">${spinner}</span>` : '') +
            '</p>'
        );
    });
    var scrollDown = function(pos) {
        var i = window.scrollY;
        var int = setInterval(function() {
          window.scrollTo(0, i);
          i += 20;
          if (i >= pos) clearInterval(int);
        }, 10);
    };
    scrollDown($('#mp-progress').prop('offsetTop'));

    // 保護を実行
    MassProtect.template = { // ウィキテキストからテンプレートを抽出する際に使用
        'names': ['pp', '保護', '半保護', '拡張半保護', '保護依頼', '保護運用', '移動保護', '移動拡張半保護'], // この名前のテンプレートを抽出
        'prefixes': ['pp-'] // この文字から始まるテンプレートを抽出
    };
    var protectCnt = 0;
    protectPages();

    /**
     * ページの保護を実行する関数 (意図的にコールバックで順番に実行)
     */
    function protectPages() {
        var pagetitle = ep.pages[protectCnt];
        var params = {
            'action': 'protect',
            'title': pagetitle,
            'protections': ep.levels,
            'expiry': ep.expiry,
            'reason': ep.reason,
            'watchlist': ep.watchlist,
            'token': mw.user.tokens.get('csrfToken'),
            'format': 'json'
        };
        if (ep.cascade) params.cascade = true;
        $.post(mw.util.wikiScript('api'), params, function(res) {

            if (res.error) { // 何らかの理由で保護に失敗
                updateProgress(pagetitle, 'protect', res.error.info);
            } else if (!res || !res.protect) { // 不明なエラー
                updateProgress(pagetitle, 'protect', false);
            } else { // 成功
                updateProgress(pagetitle, 'protect', true);
            }

            protectCnt++;
            if (ep.tag) {
                pasteTag(pagetitle);
            } else {
                if (ep.pages[protectCnt]) protectPages();
            }

        });
    }

    function pasteTag(pagetitle) {

        if (pagetitle.match(/^(?:モジュール|[Mm]odule):/)) {      // タグ添付対象がモジュール名前空間のページなら
            updateProgress(pagetitle, 'tag', 'モジュール', true); // 進捗を「中止 (モジュール)」にして
            if (ep.pages[protectCnt]) protectPages();            // タグ添付をせずに次の保護処理に移行
            return;
        }

        // タグ添付対象ページの最新版を取得
        $.get(mw.util.wikiScript('api'), {
            'action': 'query',
            'titles': pagetitle,
            'prop': 'info|revisions',
            'rvprop': 'content',
            'rvslots': 'main',
            'curtimestamp': 1,
            'format': 'json'
        }, function(res) {

            var resPages;
            if (!res || !res.query || !(resPages = res.query.pages)) {
                updateProgress(pagetitle, 'tag', '通信エラー', true);
                if (ep.pages[protectCnt]) protectPages();
                return;
            }

            var curtimestamp = res.curtimestamp;
            for (var key in resPages) { // キーはランダムナンバー (ページID)

                var resObj = resPages[key];
                var pageMissing = resObj.missing === '';
                var isRedirect = resObj.redirect === '';

                // ページが未作成の場合はタグ添付をしない
                if (pageMissing) {
                    updateProgress(pagetitle, 'tag', '未作成', true);
                    if (ep.pages[protectCnt]) protectPages();
                    return;
                }

                // タグ添付するページの編集に必要な情報を取得
                var basetimestamp = resObj.touched;
                var content = resObj.revisions[0].slots.main['*']; // ページ内容を取得
                var originalContent = JSON.parse(JSON.stringify(content)); // 後で比較する用の魚拓
                var templates = findTemplates(content, MassProtect.template.names, MassProtect.template.prefixes); // 除去対象のテンプレートを取得
                if (templates.length !== 0) {
                    templates = templates.map(function(item) {
                        return mw.util.escapeRegExp(item); // 正規表現内で使用するため特殊文字をエスケープ
                    });
                    var regex = new RegExp(`(?:${templates.join('|')})[^\\S\\n\\r]*\\n?`, 'g');
                    content = content.replace(regex, ''); // 「{{TEMPLATE}}\n」を空の文字列に置換
                }

                // 冒頭に保護タグを挿入 (リダイレクトとテンプレート名前空間のページは別処理が必要)
                if (isRedirect) { // リダイレクトなら
                    content = content.trim(); // リダイレクトのリンクの前に改行があることがあるのでそれを除去
                    if (content.indexOf('\n') === -1) { // ページ内容が転送リンクの一行のみなら
                        content += '\n' + ep.tag; // 2行目にタグを挿入
                    } else { // 複数行あれば
                        content = content.replace('\n', '\n' + ep.tag + '\n'); // 1つ目の改行の後にタグを挿入
                    }
                } else {
                    if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):.+\.css$/)) { // .cssページなら
                        content = '/*' + ep.tag + '*/' + '\n' + content; // コメントアウト
                    } else if (pagetitle.match(/^(?:[Tt]emplate|テンプレート):/)) { // .cssページでなければ
                        content = '<noinclude>' + ep.tag + '</noinclude>' + content; // トランスクルードさせない
                    } else {
                        content = ep.tag + '\n' + content;
                    }
                }

            }

            // 編集前と編集後で同じ内容になる場合は編集しない
            if (content === originalContent) {
                updateProgress(pagetitle, 'tag', '同じ内容', true);
                if (ep.pages[protectCnt]) protectPages();
                return;
            }

            // ページの編集
            $.post(mw.util.wikiScript('api'), {
                'action': 'edit',
                'title': pagetitle,
                'text': content,
                'summary': ep.tag + ' (MassProtect)',
                'minor': 1,
                'basetimestamp': basetimestamp,
                'starttimestamp': curtimestamp,
                'nocreate': 1,
                'watchlist': 'nochange',
                'token': mw.user.tokens.get('csrfToken'),
                'format': 'json'
            }, function(res2) {

                if (res2.error) { // 何らかの理由で編集に失敗
                    updateProgress(pagetitle, 'tag', res2.error.info);
                } else if (!res2 || !res2.edit) { // 不明なエラー
                    updateProgress(pagetitle, 'tag', false);
                } else {
                    if (res2.edit.result === 'Success') { // 成功
                        updateProgress(pagetitle, 'tag', true);
                    } else {
                        updateProgress(pagetitle, 'tag', res2.edit.result);
                    }
                }

                if (ep.pages[protectCnt]) protectPages();

            });

        });
    }

});

MassProtect.protect = { // 一括保護の進捗表示に使用
    'done': '<span class="mp-resolved" style="color: MediumSeaGreen;">完了</span>',
    'failed': '<span class="mp-resolved" style="color: MediumVioletRed;">失敗', // <span> タグは意図的に閉じない
    'cancelled': '<span class="mp-resolved">中止' // <span> タグは意図的に閉じない
};

/**
 * @param {number} page 保護対象ページ名+
 * @param {string} type 'protect' または 'tag'
 * @param {boolean|string} result 成功したらtrue、不明なエラーが発生したらfalse、理由の分かるエラーが発生したらその詳細
 *  (cancelledがtrueの場合は「中止 (XXX)」のXXXにあたる文字列)
 * @param {boolean} [cancelled]
 */
function updateProgress(page, type, result, cancelled) {

    /********************************************************************************************\
        進捗状態表示タブのHTML構造
        '<fieldset id="mp-progress">' +
            '<legend>進捗</legend>' +
            '<p>' +
                `<span class="mp-progress-page">${page}</span> ` +
                `保護: <span class="mp-progress-protect">${spinner}</span> ` +
                `タグ: <span class="mp-progress-tag">${spinner}</span>` +
            '</p>' +
        '</fieldset>'
    \********************************************************************************************/

    var $progress = $('.mp-progress-page').filter(function() {  // mp-progress-page のクラス属性を持っている <span> タグを
                        return $(this).text() === page;         // 特定のページ名を text として持つものに絞り込み
                    }).siblings(`.mp-progress-${type}`);        // 姉妹関係にあるどちらかの <span> が更新対象のセレクタ

    switch (result) {
        case true:
            $progress.prop('outerHTML', MassProtect.protect.done);
            break;
        case false:
            $progress.prop('outerHTML', `${MassProtect.protect.failed} (不明なエラー)</span>`);
            break;
        default:
            if (cancelled) {
                $progress.prop('outerHTML', `${MassProtect.protect.cancelled} (${result})</span>`);
            } else {
                $progress.prop('outerHTML', `${MassProtect.protect.failed} (${result})</span>`);
            }
    }
}

/** 
 * ウィキテキストからテンプレートを抽出
 * @param {string} wikitext
 * @param {string|Array} [templateName] テンプレート名で絞り込み
 * @param {string|Array} [templatePrefix] テンプレートの接頭辞で絞り込み
 * @returns {Array}
 */
function findTemplates(wikitext, templateName, templatePrefix) {

    // 原文を '{{' で split
    var tempInnerContent = wikitext.split('{{'); // tempInnerContent[0] は必ずテンプレートとは無関係の文字列
    if (tempInnerContent.length === 0) return [];
    var templates = [], nest = [];

    // テンプレートを抽出
    for (var i = 1; i < tempInnerContent.length; i++) { // split配列をループ
        (function() { // for のブロックスコープを作成

            var tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // 配列要素内の '}}' の数
            var temp = ''; // 値をよけておく用

            // 配列要素内に '}}' がない (= 他のテンプレートをネストしている)
            if (tempTailCnt === 0) {

                nest.push(i); // 配列要素のインデックスを記録

            // 配列要素内に '}}' が1つある (= '}}' の左側の文字列がテンプレートの引数群)
            } else if (tempTailCnt === 1) {

                temp = '{{' + tempInnerContent[i].split('}}')[0] + '}}';
                if ($.inArray(temp, templates) === -1) templates.push(temp);

            // 配列要素内に '}}' が2つ以上ある (例: 'TL2|...}}...}}'; = 他のテンプレートがネストされている)
            } else {

                for (var j = 0; j < tempTailCnt; j++) {

                    if (j === 0) { // 一番内側のテンプレート

                        temp = '{{' + tempInnerContent[i].split('}}')[j] + '}}'; // 配列要素内に '}}' が1つある時と同じ
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    } else { // 多重にネストされたテンプレート

                        var elNum = nest[nest.length -1]; // ネストの始まり部分の配列要素インデックス
                        nest.pop(); // ネストインデックスは1度参照したら用済み
                        var nestedTempInnerContent = tempInnerContent[i].split('}}'); // '}}' でsplitした新しい配列を作成

                        temp = '{{' + tempInnerContent.slice(elNum, i).join('{{') + '{{' + nestedTempInnerContent.slice(0, j + 1).join('}}') + '}}';
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    }

                }

            }

        })();
    } // ループ終了時には templates 配列に wikitext 内の全てのテンプレートが格納されている

    // テンプレートの絞り込みが不要、またはウィキテキスト内にテンプレートがない場合はここで終了
    if ((!templateName && !templatePrefix) || templates.length === 0) return templates;

    // テンプレートの絞り込み
    if (templateName && typeof templateName === 'string') templateName = [templateName];
    if (templatePrefix && typeof templatePrefix === 'string') templatePrefix = [templatePrefix];

    var caseInsensitiveFirstLetter = function(str) { // テンプレートの1文字目は大文字小文字の区別がないためその正規表現を作る関数
        return '[' + str.substring(0, 1).toUpperCase() + str.substring(0, 1).toLowerCase() + ']';
    };

    var names = [], prefixes = [];
    if (templateName) {
        for (var i = 0; i < templateName.length; i++) {
            names.push(caseInsensitiveFirstLetter(templateName[i]) + mw.util.escapeRegExp(templateName[i].substring(1)));
        }
        var templateNameRegExp = new RegExp(`^(${names.join('|')})$`);
    }
    if (templatePrefix) {
        for (var i = 0; i < templatePrefix.length; i++) {
            prefixes.push(caseInsensitiveFirstLetter(templatePrefix[i]) + mw.util.escapeRegExp(templatePrefix[i].substring(1)));
        }
        var templatePrefixRegExp = new RegExp(`^(${prefixes.join('|')})`);
    }

    templates = templates.filter(function(item) {
        var name = item.match(/^\{{2}\s*([^\|\{\}\n]+)/)[1].trim(); // {{ TEMPLATENAME | ... }} の TEMPLATENAME を抽出
        if (templateName && templatePrefix) {
            return name.match(templateNameRegExp) || name.match(templatePrefixRegExp);
        } else if (templateName) {
            return name.match(templateNameRegExp);
        } else if (templatePrefix) {
            return name.match(templatePrefixRegExp);
        }
    });

    return templates;

}

// ******************************************

})(mediaWiki, jQuery);
//</nowiki>