// <nowiki>
/**
* QuickRating
* 為模板與模組提供快速新增、修改或清除評級模板的工具
var _addText = "{{工具警告|QuickRating|page=程式碼}}";
var _addText = "{{Notice|<big>'''文檔請見:[[User:Addonian1123/QuickRating]]'''</big>}}";
*/
$(() => {
// 僅在模板(10, 11)與模組(828, 829)命名空間執行
const ns = mw.config.get('wgNamespaceNumber');
if (![10, 11, 828, 829].includes(ns)) return;
// 新增工具列按鈕
const portletLink = mw.util.addPortletLink(
'p-cactions',
'#',
'評級此模板/模組',
'ca-rating-tool',
'[QuickRating] 為此模板或模組評級'
);
if (!portletLink) return;
$(portletLink).on('click', (e) => {
e.preventDefault();
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'oojs-ui-core', 'oojs-ui-windows', 'oojs-ui-widgets']).then(initTool);
});
// 階段評級對照表
const phaseAliasMap = {
'pre-alpha': 'pre-alpha', 'prealpha': 'pre-alpha', 'pa': 'pre-alpha',
'alpha': 'alpha', 'a': 'alpha',
'beta': 'beta', 'b': 'beta',
'release': 'release', 'r': 'release', 'general': 'release', 'g': 'release', 'stable': 'release',
'broken': 'broken', 'br': 'broken', 'unstable': 'broken',
'deprecated': 'deprecated', 'd': 'deprecated', 'defunct': 'deprecated'
};
// 保護狀態對照表
const protectionAliasMap = {
'semiprotected': 'semiprotected', 'semiprotect': 'semiprotected', 'semi': 'semiprotected',
'protected': 'protected', 'protect': 'protected', 'p': 'protected'
};
function initTool() {
let titleObj = new mw.Title(mw.config.get('wgPageName'));
if (titleObj.isTalkPage()) {
titleObj = titleObj.getSubjectPage();
}
const pageName = titleObj.getPrefixedText();
const api = new mw.Api();
api.get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
titles: pageName,
formatversion: 2
}).done((data) => {
const page = data?.query?.pages?.[0];
const content = (page && !page.missing) ? (page.revisions?.[0]?.slots?.main?.content || '') : '';
const isDocPage = pageName.endsWith('/doc') || /\{\{\s*(Documentation subpage|模板文档子页面|說明文件子頁面)/i.test(content);
let targetDocName = pageName;
if (isDocPage) {
showDialog(api, targetDocName, content);
} else {
const cleanContent = content.replace(/<!--[\s\S]*?-->/g, '').replace(/<nowiki>[\s\S]*?<\/nowiki>/gi, '');
const docAliases = 'Documentation|Doc|Documentation, template|Documentations|Template doc|Template doc page|Template doc page transcluded|模板文件|模板文档|說明文件';
const docRegex = new RegExp(`\\{\\{\\s*(${docAliases})\\s*(\\|[^\\}]*)?\\}\\}`, 'i');
const match = cleanContent.match(docRegex);
targetDocName = `${pageName}/doc`;
if (match?.[2]) {
const params = match[2].split('|');
for (let i = 1; i < params.length; i++) {
const param = params[i].trim();
if (param.startsWith('1=')) {
targetDocName = param.substring(2).trim();
break;
} else if (!param.includes('=')) {
targetDocName = param;
break;
}
}
}
api.get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
titles: targetDocName,
formatversion: 2
}).done((docData) => {
const docPage = docData?.query?.pages?.[0];
const docContent = (docPage && !docPage.missing) ? (docPage.revisions?.[0]?.slots?.main?.content || '') : '';
showDialog(api, targetDocName, docContent);
}).fail((err) => {
OO.ui.alert(`無法讀取子頁面內容:${err}`);
});
}
}).fail((err) => {
OO.ui.alert(`無法讀取頁面內容:${err}`);
});
}
function showDialog(api, targetDocName, docContent) {
const isModule = /^(Module|模组|模組):/i.test(targetDocName) || [828, 829].includes(ns);
const templateName = isModule ? 'Module rating' : 'Template rating';
const phaseOptions = [
{ data: 'pre-alpha', label: 'Pre-Alpha版' },
{ data: 'alpha', label: 'Alpha版' },
{ data: 'beta', label: 'Beta測試版' },
{ data: 'release', label: '穩定版' },
{ data: 'broken', label: '已損壞' },
{ data: 'deprecated', label: '已廢棄' }
];
const protectionOptions = [
{ data: 'semiprotected', label: '半保護' },
{ data: 'protected', label: '全保護' }
];
// 偵測頁面中所有現存評級模板參數
const existingRegexGlobal = new RegExp(`\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|([^}]+)\\}\\}`, 'gi');
const detectedParamsList = [];
let match;
while ((match = existingRegexGlobal.exec(docContent)) !== null) {
detectedParamsList.push(match[1].trim());
}
const initialParam1 = detectedParamsList[0] || '';
const initialParam2 = detectedParamsList[1] || '';
function CustomRatingDialog(config) {
CustomRatingDialog.super.call(this, config);
}
OO.inheritClass(CustomRatingDialog, OO.ui.ProcessDialog);
CustomRatingDialog.static.name = 'customRatingDialog';
CustomRatingDialog.static.size = 'medium';
CustomRatingDialog.static.title = new OO.ui.HtmlSnippet(
`<a href="${mw.util.getUrl('User:Addonian1123/QuickRating')}" target="_blank" style="color: inherit; text-decoration: underline;">QuickRating</a>:設定${isModule ? '模組' : '模板'}評級`
);
CustomRatingDialog.static.actions = [
{ action: 'save', label: '儲存變更', flags: ['primary', 'progressive'] },
{ action: 'close', icon: 'close', flags: ['safe', 'close'], title: '關閉' }
];
CustomRatingDialog.prototype.initialize = function () {
CustomRatingDialog.super.prototype.initialize.apply(this, arguments);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
// 目標頁面輸入框
this.targetInput = new OO.ui.TextInputWidget({ value: targetDocName });
// 階段評級 RadioSelect
this.phaseRadio = new OO.ui.RadioSelectWidget();
phaseOptions.forEach(opt => {
this.phaseRadio.addItems([
new OO.ui.RadioOptionWidget({ data: opt.data, label: opt.label })
]);
});
// 特殊狀態 Checkbox 與 RadioSelect
this.specialCheckbox = new OO.ui.CheckboxInputWidget({ selected: false });
this.protectionRadio = new OO.ui.RadioSelectWidget();
protectionOptions.forEach(opt => {
this.protectionRadio.addItems([
new OO.ui.RadioOptionWidget({ data: opt.data, label: opt.label })
]);
});
// 自訂參數輸入框
this.paramInput1 = new OO.ui.TextInputWidget({
value: initialParam1,
placeholder: '例如:release|nocat=true'
});
this.paramInput2 = new OO.ui.TextInputWidget({
value: initialParam2,
placeholder: '例如:protected|nocat=true'
});
// 初始化表單狀態
this.parseInitialState(initialParam1, initialParam2);
// 單向點擊事件:僅在點選 Radio 時填入首項參數
this.phaseRadio.on('select', (item) => {
if (!item) return;
const val = item.getData();
const currentText = this.paramInput1.getValue().trim();
if (!currentText) {
this.paramInput1.setValue(val);
} else {
const parts = currentText.split('|').map(s => s.trim());
let replaced = false;
for (let i = 0; i < parts.length; i++) {
if (phaseAliasMap[parts[i].toLowerCase()]) {
parts[i] = val;
replaced = true;
break;
}
}
if (!replaced) parts.unshift(val);
this.paramInput1.setValue(parts.join('|'));
}
});
this.protectionRadio.on('select', (item) => {
if (!item || !this.specialCheckbox.isSelected()) return;
const val = item.getData();
const currentText = this.paramInput2.getValue().trim();
if (!currentText) {
this.paramInput2.setValue(val);
} else {
const parts = currentText.split('|').map(s => s.trim());
let replaced = false;
for (let i = 0; i < parts.length; i++) {
if (protectionAliasMap[parts[i].toLowerCase()]) {
parts[i] = val;
replaced = true;
break;
}
}
if (!replaced) parts.unshift(val);
this.paramInput2.setValue(parts.join('|'));
}
});
// 切換特殊狀態開關
const toggleSpecial = (enabled) => {
this.protectionRadio.setDisabled(!enabled);
this.paramInput2.setDisabled(!enabled);
if (enabled && !this.paramInput2.getValue().trim()) {
const selected = this.protectionRadio.findSelectedItem();
if (selected) {
this.paramInput2.setValue(selected.getData());
} else {
this.protectionRadio.selectItemByData('semiprotected');
this.paramInput2.setValue('semiprotected');
}
}
};
this.specialCheckbox.on('change', toggleSpecial);
toggleSpecial(this.specialCheckbox.isSelected());
// 清除全部模板按鈕
const clearAllBtn = new OO.ui.ButtonWidget({
label: '清除全部模板', flags: ['destructive'], framed: true
});
clearAllBtn.$element.css({ 'float': 'left', 'margin-top': '12px' });
clearAllBtn.on('click', () => {
const customTarget = this.targetInput.getValue().trim();
document.activeElement?.blur();
const manager = this.getManager();
this.close().closed.then(() => {
manager?.destroy();
if (!customTarget) {
OO.ui.alert('請輸入有效的目標頁面名稱。').done(() => showDialog(api, targetDocName, docContent));
return;
}
OO.ui.confirm(`確定要清除「${customTarget}」上的所有評級模板嗎?`).done((confirmed) => {
if (confirmed) {
saveRating(api, templateName, [], customTarget, docContent, null);
} else {
showDialog(api, targetDocName, docContent);
}
});
});
});
// 表單組裝
const fieldset = new OO.ui.FieldsetLayout({ label: '評級設定' });
fieldset.addItems([
new OO.ui.FieldLayout(this.targetInput, {
label: `目標${isModule ? '模組' : '模板'}子頁面:`,
align: 'top'
}),
new OO.ui.FieldLayout(this.phaseRadio, {
label: '階段評級:',
align: 'top'
}),
new OO.ui.FieldLayout(this.specialCheckbox, {
label: '特殊狀態',
align: 'inline'
}),
new OO.ui.FieldLayout(this.protectionRadio, {
label: '保護狀態(僅在「特殊狀態」啟用時生效):',
align: 'top'
}),
new OO.ui.FieldLayout(this.paramInput1, {
label: '階段評級自訂參數:',
align: 'top'
}),
new OO.ui.FieldLayout(this.paramInput2, {
label: '特殊狀態自訂參數(僅在「特殊狀態」啟用時生效):',
align: 'top'
})
]);
const footerContainer = $('<div>').css({ 'clear': 'both' }).append(clearAllBtn.$element);
this.panel.$element.append(fieldset.$element, footerContainer);
this.$body.append(this.panel.$element);
};
CustomRatingDialog.prototype.parseInitialState = function (p1, p2) {
let matchedPhase = null;
let matchedProt = null;
if (p1) {
const parts = p1.split('|').map(s => s.trim());
for (const part of parts) {
const lower = part.toLowerCase();
if (phaseAliasMap[lower]) {
matchedPhase = phaseAliasMap[lower];
break;
}
if (!matchedProt && protectionAliasMap[lower]) {
matchedProt = protectionAliasMap[lower];
}
}
}
if (p2) {
const parts = p2.split('|').map(s => s.trim());
for (const part of parts) {
const lower = part.toLowerCase();
if (protectionAliasMap[lower]) {
matchedProt = protectionAliasMap[lower];
break;
}
}
}
if (matchedPhase) {
this.phaseRadio.selectItemByData(matchedPhase);
}
if (p2 || matchedProt) {
this.specialCheckbox.setSelected(true);
if (matchedProt) {
this.protectionRadio.selectItemByData(matchedProt);
}
}
};
CustomRatingDialog.prototype.getActionProcess = function (action) {
if (action === 'save') {
return new OO.ui.Process(() => {
const customTarget = this.targetInput.getValue().trim();
if (!customTarget) {
return Promise.reject(new OO.ui.Error('請輸入有效的目標頁面名稱。', { warning: true }));
}
const val1 = this.paramInput1.getValue().trim();
const val2 = this.paramInput2.getValue().trim();
const isSpecial = this.specialCheckbox.isSelected();
if (!isSpecial && val2) {
return Promise.reject(new OO.ui.Error('若要設定第二個自訂參數,請先勾選「特殊狀態」。', { warning: true }));
}
const paramStrings = [];
if (val1) paramStrings.push(val1);
if (isSpecial && val2) paramStrings.push(val2);
return saveRating(api, templateName, paramStrings, customTarget, docContent, this);
});
}
if (action === 'close') {
return new OO.ui.Process(() => {
document.activeElement?.blur();
const manager = this.getManager();
return this.close().closed.then(() => {
manager?.destroy();
});
});
}
return CustomRatingDialog.super.prototype.getActionProcess.call(this, action);
};
const windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
const dialog = new CustomRatingDialog();
windowManager.addWindows([dialog]);
windowManager.openWindow(dialog);
}
function saveRating(api, templateName, allParamStrings, targetDocName, actualContent, dialog) {
const existingRegexGlobal = new RegExp(`\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|[^}]*\\}\\}\\n?`, 'gi');
let newContent = actualContent;
if (allParamStrings.length === 0) {
if (existingRegexGlobal.test(actualContent)) {
existingRegexGlobal.lastIndex = 0;
newContent = actualContent.replace(existingRegexGlobal, '').replace(/\n\n\n+/g, '\n\n').trim();
} else {
if (dialog) {
document.activeElement?.blur();
const manager = dialog.getManager();
dialog.close().closed.then(() => manager?.destroy());
}
OO.ui.alert('該頁面目前未掛有評級模板,未進行任何變更。').done(() => {
showDialog(api, targetDocName, actualContent);
});
return Promise.resolve();
}
} else {
const generatedTemplates = allParamStrings.map(p => `{{${templateName}|${p}}}`);
const newTplBlock = `${generatedTemplates.join('')}`;
const replaceRegex = new RegExp(`(\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|[^}]*\\}\\})+`, 'i');
if (replaceRegex.test(actualContent)) {
newContent = actualContent.replace(replaceRegex, newTplBlock);
} else {
const docSubRegex = /\{\{\s*(Documentation subpage|模板文档子页面|說明文件子頁面)[^\\}]*\}\}\n?/i;
if (docSubRegex.test(actualContent)) {
newContent = actualContent.replace(docSubRegex, `$&${newTplBlock}`);
} else {
newContent = newTplBlock + actualContent;
}
}
}
const isExisting = new RegExp(`\\{\\{\\s*${templateName.replace(' ', '[ _]')}\\s*\\|[^}]*\\}\\}`, 'i').test(actualContent);
const actionText = isExisting ? '變更' : '新增';
const summaryText = (allParamStrings.length === 0)
? `清除所有評級模板([[Template:${templateName}]])// [[User:Addonian1123/QuickRating|QuickRating]]`
: `${actionText}評級模板([[Template:${templateName}]])為:${allParamStrings.join(' / ')} // [[User:Addonian1123/QuickRating|QuickRating]]`;
return api.postWithToken('csrf', {
action: 'edit',
title: targetDocName,
text: newContent,
summary: summaryText
}).then(() => {
if (dialog) {
document.activeElement?.blur();
const manager = dialog.getManager();
dialog.close().closed.then(() => manager?.destroy());
}
const successMsg = (allParamStrings.length === 0 ? '已成功清除所有評級模板。' : `已成功${actionText}評級模板。`);
showCompletionDialog(api, targetDocName, successMsg);
}, (err) => {
if (dialog) {
document.activeElement?.blur();
const manager = dialog.getManager();
dialog.close().closed.then(() => manager?.destroy());
}
OO.ui.alert(`儲存失敗:${err}`).done(() => {
showDialog(api, targetDocName, actualContent);
});
});
}
function showCompletionDialog(api, targetDocName, messageText) {
const windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
const messageDialog = new OO.ui.MessageDialog();
windowManager.addWindows([messageDialog]);
windowManager.openWindow(messageDialog, {
title: '操作成功',
message: messageText,
actions: [
{ action: 'purge', label: '清除快取', flags: ['primary', 'progressive'] },
{ action: 'reload', label: '重新載入頁面', flags: ['safe'] }
]
}).closed.then((data) => {
document.activeElement?.blur();
windowManager.destroy();
if (data?.action === 'purge') {
mw.notify('正在清除頁面快取...');
api.post({
action: 'purge',
titles: [mw.config.get('wgPageName'), targetDocName],
forcelinkupdate: 1
}).always(() => {
location.reload();
});
} else {
location.reload();
}
});
}
});
// </nowiki>