User:Chatsven/NoReferenceFinder.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
//一个可以检索疑似没有参考来源、来源不足、参考格式错误的条目,并人工添加维护模板的脚本。
//反馈:[[User talk:Chatsven/NoReferenceFinder.js]]
//------------------------------------------------
(function () {
'use strict';
const TOOL_NAME = '无来源条目检索';
const DEFAULT_LIMIT = 50;
let nrfCurrentItem = null;
let nrfCurrentItemId = null;
mw.loader.using(['mediawiki.api', 'mediawiki.util'], function () {
addStyles();
addPortletButton();
});
function addStyles() {
if ($('#no-reference-finder-style').length) {
return;
}
$('head').append(
$('<style>')
.attr('id', 'no-reference-finder-style')
.text(`
#nrf-overlay,
.nrf-sidebar-list,
.nrf-preview-area {
scrollbar-width: none;
-ms-overflow-style: none;
}
#nrf-overlay::-webkit-scrollbar,
.nrf-sidebar-list::-webkit-scrollbar,
.nrf-preview-area::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
@keyframes nrf-window-open {
0% {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes nrf-window-close {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
}
@keyframes nrf-detail-overlay-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes nrf-detail-overlay-fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.nrf-overlay {
position: fixed;
inset: 0;
z-index: 9998;
background: rgba(32, 33, 36, 0.28);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 12px;
line-height: 1.35;
}
.nrf-window {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 900px;
height: 600px;
box-sizing: border-box;
padding: 8px;
border-radius: 26px;
background: #fff;
overflow: hidden;
z-index: 9999;
box-shadow: 0 8px 40px 0 rgba(0, 0, 0, 0.12);
animation: nrf-window-open 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
transform-origin: center center;
}
.nrf-overlay.nrf-closing .nrf-window {
animation: nrf-window-close 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
}
.nrf-sidebar {
position: absolute;
left: 8px;
top: 8px;
width: 240px;
height: 584px;
box-sizing: border-box;
border-radius: 18px;
border: 1px solid #F0F0F0;
background: #FFFFFF;
overflow: hidden;
}
.nrf-close-dot {
position: absolute;
left: 11px;
top: 11px;
width: 14px;
height: 14px;
box-sizing: border-box;
border-radius: 100px;
border: 0.5px solid rgba(0, 0, 0, 0.10);
background: #FF6F68;
cursor: pointer;
padding: 0;
box-shadow: 0 8px 40px 0 rgba(0, 0, 0, 0.12);
z-index: 20;
}
.nrf-close-dot:focus {
outline: none;
}
.nrf-sidebar-list {
position: absolute;
left: 0;
top: 74px;
right: 0;
bottom: 0;
overflow: auto;
padding: 0 10px 16px 10px;
box-sizing: border-box;
}
.nrf-sidebar-list.nrf-sidebar-changing .nrf-list-item-selected {
background: rgba(227, 227, 227, 0);
}
.nrf-group {
margin-bottom: 16px;
}
.nrf-group-title {
margin: 0 0 12px 10px;
color: rgba(0, 0, 0, 0.50);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 10px;
font-style: normal;
font-weight: 700;
line-height: 13px;
mix-blend-mode: plus-darker;
}
.nrf-list-item {
display: flex;
height: 24px;
padding: 0 10px 0 6px;
align-items: center;
gap: 4px;
align-self: stretch;
box-sizing: border-box;
border-radius: 8px;
cursor: pointer;
margin-bottom: 4px;
color: rgba(0, 0, 0, 0.85);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 700;
line-height: 15px;
mix-blend-mode: plus-darker;
overflow: hidden;
}
.nrf-list-item-selected {
background: rgba(227, 227, 227, 1);
color: #007AFF;
transition: background 0.2s ease;
}
.nrf-list-item-selected .nrf-list-item-text {
color: #007AFF;
}
.nrf-list-item-added {
color: rgba(0, 0, 0, 0.28) !important;
}
.nrf-list-item-added .nrf-list-item-text {
color: rgba(0, 0, 0, 0.28) !important;
}
.nrf-list-item-added.nrf-list-item-selected {
background: rgba(227, 227, 227, 0.55);
}
.nrf-list-item-text {
flex: 1 0 0;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.nrf-content {
position: absolute;
left: 248px;
top: 8px;
right: 8px;
bottom: 8px;
display: flex;
padding: 20px;
align-items: flex-start;
box-sizing: border-box;
background: transparent;
}
.nrf-current-title {
position: absolute;
left: 20px;
top: 10px;
right: 120px;
color: rgba(0, 0, 0, 0.85);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 16px;
mix-blend-mode: plus-darker;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 10;
}
.nrf-top-actions {
position: absolute;
top: 0;
right: 0;
width: 84px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 100px;
border: 1px solid #F0F0F0;
background: #FFFFFF;
overflow: hidden;
box-shadow: none;
z-index: 30;
}
.nrf-top-button {
position: relative;
z-index: 31;
flex: 1 1 0;
height: 100%;
border: 0;
background: transparent;
color: rgba(0, 0, 0, 0.85);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 13px;
font-weight: 700;
line-height: 16px;
cursor: pointer;
padding: 0;
}
.nrf-top-button:focus {
outline: none;
}
.nrf-top-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.nrf-preview-area {
position: absolute;
left: 20px;
right: 8px;
top: 52px;
bottom: 0;
overflow: hidden;
background: transparent;
scrollbar-width: none;
-ms-overflow-style: none;
z-index: 1;
}
.nrf-preview-area::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 40px;
pointer-events: none;
z-index: 3;
opacity: 0;
transition: opacity 0.18s ease;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
}
.nrf-preview-area.nrf-preview-scrolled::before {
opacity: 1;
}
.nrf-preview-area::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.nrf-preview-frame {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
border: 0;
background: #fff;
display: block;
opacity: 1;
transition: opacity 0.2s ease;
}
.nrf-preview-frame.nrf-preview-hidden {
opacity: 0;
}
.nrf-empty-preview {
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
font-weight: 600;
}
.nrf-window.nrf-detail-open {
overflow: hidden;
}
.nrf-detail-layer {
position: absolute;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 26px;
background: rgba(0, 0, 0, 0.18);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
opacity: 0;
animation: nrf-detail-overlay-fade-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.nrf-detail-layer.nrf-detail-closing {
pointer-events: none;
animation: nrf-detail-overlay-fade-out 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
}
.nrf-detail-card {
position: absolute;
left: 50%;
top: 50%;
width: 390px;
min-height: 330px;
height: 330px;
transform: translate(-50%, -50%);
box-sizing: border-box;
border-radius: 26px;
background: #FFFFFF;
overflow: hidden;
animation: nrf-window-open 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
transform-origin: center center;
}
.nrf-detail-layer.nrf-detail-closing .nrf-detail-card {
animation: nrf-window-close 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
}
.nrf-detail-title {
position: absolute;
left: 120px;
top: 20px;
right: 31px;
color: rgba(0, 0, 0, 0.85);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 700;
line-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nrf-detail-subtitle {
position: absolute;
left: 120px;
top: 44px;
right: 31px;
color: rgba(0, 0, 0, 0.85);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 14px;
}
.nrf-detail-row-label {
position: absolute;
left: 34px;
display: flex;
width: 80px;
height: 24px;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.50);
text-align: right;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 14px;
letter-spacing: -0.26px;
}
.nrf-detail-row-value {
position: absolute;
left: 126px;
right: 31px;
display: -webkit-box;
height: 24px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
align-items: center;
overflow: hidden;
color: rgba(0, 0, 0, 0.85);
text-overflow: ellipsis;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 510;
line-height: 16px;
mix-blend-mode: plus-darker;
}
.nrf-detail-row-value > span {
display: flex;
height: 24px;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nrf-detail-row-value.nrf-detail-summary {
display: block;
height: auto;
min-height: 24px;
line-height: 16px;
overflow: visible;
text-overflow: clip;
-webkit-line-clamp: unset;
-webkit-box-orient: initial;
}
.nrf-detail-row-value.nrf-detail-summary > span {
display: block;
height: auto;
max-height: none;
align-items: initial;
overflow: visible;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
text-overflow: clip;
}
.nrf-detail-row-1 {
top: 96px;
}
.nrf-detail-row-2 {
top: 134px;
}
.nrf-detail-row-3 {
top: 172px;
}
.nrf-detail-row-4 {
top: 210px;
}
.nrf-detail-row-5 {
top: 248px;
}
.nrf-detail-actions {
position: absolute;
right: 20px;
bottom: 20px;
display: flex;
gap: 8px;
}
.nrf-detail-button {
display: flex;
width: 74px;
height: 24px;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 8px;
cursor: pointer;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 510;
line-height: 16px;
mix-blend-mode: plus-darker;
}
.nrf-detail-button:focus {
outline: none;
}
.nrf-detail-close {
background: rgba(0, 0, 0, 0.06);
color: #1A1A1A;
}
.nrf-detail-add {
background: #007AFF;
color: #FFFFFF;
}
.nrf-detail-add:disabled {
opacity: 0.45;
cursor: not-allowed;
}
`)
);
}
function addPortletButton() {
$('#ca-no-reference-finder').remove();
mw.util.addPortletLink(
'p-cactions',
'#',
'检索来源',
'ca-no-reference-finder',
'检索疑似没有参考来源、来源不足或参考格式错误的条目'
);
$('#ca-no-reference-finder').on('click', function (e) {
e.preventDefault();
startSearch();
});
}
function startSearch() {
const keyword = prompt(
'请输入检索关键词。\n例如:人物、公司、学校、电影。\n留空则随机检索部分条目。',
''
);
mw.notify('正在检索 50 个候选条目……', {
title: TOOL_NAME,
type: 'info'
});
if (keyword && keyword.trim()) {
searchByKeyword(keyword.trim(), DEFAULT_LIMIT);
} else {
getRandomPages(DEFAULT_LIMIT);
}
}
function searchByKeyword(keyword, limit) {
const api = new mw.Api();
api.get({
action: 'query',
list: 'search',
srnamespace: 0,
srlimit: limit,
srsearch: keyword,
format: 'json'
}).done(function (data) {
const titles = data.query.search.map(function (item) {
return item.title;
});
inspectPages(titles);
}).fail(function (code, result) {
console.error(code, result);
mw.notify('搜索失败:' + code, {
title: TOOL_NAME,
type: 'error'
});
});
}
function getRandomPages(limit) {
const api = new mw.Api();
api.get({
action: 'query',
list: 'random',
rnnamespace: 0,
rnlimit: limit,
format: 'json'
}).done(function (data) {
const titles = data.query.random.map(function (item) {
return item.title;
});
inspectPages(titles);
}).fail(function (code, result) {
console.error(code, result);
mw.notify('随机取样失败:' + code, {
title: TOOL_NAME,
type: 'error'
});
});
}
function inspectPages(titles) {
if (!titles.length) {
mw.notify('没有找到候选条目。', {
title: TOOL_NAME,
type: 'warn'
});
return;
}
const api = new mw.Api();
api.get({
action: 'query',
prop: 'revisions|info',
titles: titles.join('|'),
rvprop: 'content',
rvslots: 'main',
formatversion: 2,
format: 'json'
}).done(function (data) {
const candidates = [];
data.query.pages.forEach(function (page) {
if (page.missing || !page.revisions || !page.revisions[0]) {
return;
}
const text = page.revisions[0].slots.main.content;
const referenceCount = countReferences(text);
const bodyLength = countBodyTextLength(text);
const hasReferenceHeading = hasReferencesHeading(text);
const advice = getTemplateAdvice(page.title, text, referenceCount, hasReferenceHeading, bodyLength);
if (advice.shouldTag) {
candidates.push({
title: page.title,
text: text,
referenceCount: referenceCount,
bodyLength: bodyLength,
hasReferenceHeading: hasReferenceHeading,
advice: advice,
demand: getDemandLevel(advice.type, referenceCount, hasReferenceHeading, bodyLength)
});
}
});
showCandidates(candidates);
}).fail(function (code, result) {
console.error(code, result);
mw.notify('读取页面内容失败:' + code, {
title: TOOL_NAME,
type: 'error'
});
});
}
function countReferences(text) {
let count = 0;
const refMatches = text.match(/<ref[\s>\/]/gi);
if (refMatches) {
count += refMatches.length;
}
const citeMatches = text.match(/\{\{\s*(cite web|cite news|cite book|cite journal|cite encyclopedia|citation)\b/gi);
if (citeMatches) {
count += citeMatches.length;
}
const zhCiteMatches = text.match(/\{\{\s*(引用网页|引用網頁|引用新聞|引用新闻|引用書籍|引用书籍|引用期刊|引用文献|引用來源|引用来源)\b/gi);
if (zhCiteMatches) {
count += zhCiteMatches.length;
}
return count;
}
function countBodyTextLength(text) {
let body = text;
body = body.split(/(^|\n)==+\s*(参考资料|參考資料|参考文献|參考文獻|参考来源|參考來源|參見資料|注释|註釋|备注|備註|外部链接|外部連結|参见|參見|相关条目|相關條目)\s*==+/i)[0];
body = body.replace(/<ref\b[^>]*>[\s\S]*?<\/ref>/gi, '');
body = body.replace(/<ref\b[^\/>]*\/\s*>/gi, '');
body = body.replace(/\{\|[\s\S]*?\|\}/g, '');
for (let i = 0; i < 8; i++) {
body = body.replace(/\{\{[^{}]*\}\}/g, '');
}
body = body.replace(/\[\[(File|Image|文件|圖像|图像|Category|分類|分类|[a-z-]+):[^\]]+\]\]/gi, '');
body = body.replace(/\[\[[^\]|]+\|([^\]]+)\]\]/g, '$1');
body = body.replace(/\[\[([^\]]+)\]\]/g, '$1');
body = body.replace(/\[https?:\/\/[^\s\]]+\s+([^\]]+)\]/gi, '$1');
body = body.replace(/\[https?:\/\/[^\]]+\]/gi, '');
body = body.replace(/<[^>]+>/g, '');
body = body.replace(/={2,}/g, '');
body = body.replace(/^[*#:;]+/gm, '');
body = body.replace(/'{2,5}/g, '');
body = body.replace(/__[^_]+__/g, '');
const chars = body.match(/[\u4e00-\u9fffA-Za-z0-9]/g);
return chars ? chars.length : 0;
}
function hasReferencesHeading(text) {
return /(^|\n)==+\s*(参考资料|參考資料|参考文献|參考文獻|参考来源|參考來源|參見資料|注释|註釋|备注|備註)\s*==+/i.test(text);
}
function isDisambiguationPage(title, text) {
if (/\((消歧义|消歧義|disambiguation)\)$/i.test(title)) {
return true;
}
if (/(消歧义|消歧義|Disambiguation)$/i.test(title)) {
return true;
}
if (/\{\{\s*(disambiguation|disambig|dab|geodis|hndis|numberdis|surname|given name|消歧义|消歧義|消歧义页|消歧義頁|消歧义页面|消歧義頁面)\b/i.test(text)) {
return true;
}
if (/\[\[\s*(Category|分類|分类)\s*:\s*(消歧义页|消歧義頁|消歧义页面|消歧義頁面|Disambiguation pages)/i.test(text)) {
return true;
}
if (/(本页|此页|本頁|此頁|本條目|本条目|本文)\s*(是|為|为)?\s*(消歧义|消歧義)/i.test(text)) {
return true;
}
return false;
}
function hasExistingSourceMaintenanceTemplate(text) {
return /(?:\{\{\s*(unreferenced|refimprove|no[\s_-]*footnotes|more[\s_-]*citations[\s_-]*needed|citations[\s_-]*missing|citation[\s_-]*needed|primary[\s_-]*sources|third[\s_-]*party|BLP[\s_-]*unsourced|sources[\s_-]*exist|來源不足|来源不足|無來源|无来源|缺少來源|缺少来源|沒有腳註|没有脚注|沒有註腳|没有注脚|未列明來源|未列明来源|需要更多來源|需要更多来源|一次來源|一次来源|第三方來源|第三方来源)\b)/i.test(text);
}
function getTemplateAdvice(title, text, referenceCount, hasReferenceHeading, bodyLength) {
if (/^#redirect/i.test(text)) {
return { shouldTag: false };
}
if (isDisambiguationPage(title, text)) {
return { shouldTag: false };
}
if (hasExistingSourceMaintenanceTemplate(text)) {
return { shouldTag: false };
}
if (referenceCount === 0 && hasReferenceHeading) {
return {
shouldTag: true,
type: 'format',
templateName: 'no footnotes',
templateText: '{{no footnotes|time=' + getToday() + '}}\n',
label: '建议使用 {{no footnotes}}',
reason: '有参考资料章节标题,但没有 <ref> 脚注或常见引用模板。'
};
}
if (referenceCount === 0) {
return {
shouldTag: true,
type: 'unreferenced',
templateName: 'unreferenced',
templateText: '{{unreferenced|time=' + getToday() + '}}\n',
label: '建议使用 {{unreferenced}}',
reason: '没有 <ref> 标签、常见引用模板,也没有明显的参考资料章节。'
};
}
const refImproveReason = getRefImproveReason(referenceCount, bodyLength);
if (refImproveReason) {
return {
shouldTag: true,
type: 'refimprove',
templateName: 'refimprove',
templateText: '{{refimprove|time=' + getToday() + '}}\n',
label: '建议使用 {{refimprove}}',
reason: refImproveReason
};
}
return { shouldTag: false };
}
function getRefImproveReason(referenceCount, bodyLength) {
if (bodyLength < 800) {
return false;
}
if (bodyLength >= 800 && bodyLength < 2000 && referenceCount <= 2) {
return '正文约 ' + bodyLength + ' 字符,已有 ' + referenceCount + ' 个参考来源,来源数量偏少。';
}
if (bodyLength >= 2000 && bodyLength < 5000 && referenceCount <= 4) {
return '正文约 ' + bodyLength + ' 字符,已有 ' + referenceCount + ' 个参考来源,来源覆盖可能不足。';
}
if (bodyLength >= 5000 && referenceCount <= 6) {
return '正文约 ' + bodyLength + ' 字符,已有 ' + referenceCount + ' 个参考来源,来源数量明显不足。';
}
return false;
}
function buildEditSummary(templateName, adviceType, referenceCount, bodyLength, hasReferenceHeading) {
let summary = '添加{{' + templateName + '}}:';
if (adviceType === 'unreferenced') {
summary += '條目未列明參考來源;正文約' + bodyLength + '字符;';
summary += hasReferenceHeading ? '已有參考資料章節但未見有效引用' : '未見參考資料章節';
} else if (adviceType === 'format') {
summary += '已有參考資料章節,但未見腳註或常見引用模板;正文約' + bodyLength + '字符';
} else if (adviceType === 'refimprove') {
summary += '條目篇幅約' + bodyLength + '字符,目前僅有' + referenceCount + '個參考來源,來源覆蓋可能不足';
} else {
summary += '條目來源狀況需要維護';
}
if (summary.length > 230) {
summary = summary.slice(0, 227) + '...';
}
return summary;
}
function getDemandLevel(type, referenceCount, hasReferenceHeading, bodyLength) {
if (type === 'unreferenced') {
return { level: '高需求', group: '高需求', label: '高需求' };
}
if (type === 'format') {
return { level: '格式錯誤', group: '格式錯誤', label: '格式錯誤' };
}
if (type === 'refimprove') {
return { level: '中需求', group: '中需求', label: '中需求' };
}
return { level: '低需求', group: '低需求', label: '低需求' };
}
function showCandidates(candidates) {
if (!candidates.length) {
mw.notify('没有发现明显需要添加来源维护模板的条目。', {
title: TOOL_NAME,
type: 'success'
});
return;
}
$('#nrf-overlay').remove();
nrfCurrentItem = null;
nrfCurrentItemId = null;
const $overlay = $('<div>')
.attr('id', 'nrf-overlay')
.addClass('nrf-overlay');
const $window = $('<div>').addClass('nrf-window');
const $sidebar = $('<div>').addClass('nrf-sidebar');
const $close = $('<button>')
.attr('type', 'button')
.attr('aria-label', '关闭')
.addClass('nrf-close-dot')
.on('click', function (e) {
e.preventDefault();
closeNrfWindow();
});
const $sidebarList = $('<div>').addClass('nrf-sidebar-list');
const $content = $('<div>').addClass('nrf-content');
const $title = $('<div>')
.attr('id', 'nrf-current-title')
.addClass('nrf-current-title')
.text('條目名稱');
const $actions = $('<div>').addClass('nrf-top-actions');
const $addButton = $('<button>')
.attr('type', 'button')
.attr('id', 'nrf-current-add')
.addClass('nrf-top-button')
.text('添加')
.on('click', function (e) {
e.preventDefault();
addCurrentItem();
});
const $detailButton = $('<button>')
.attr('type', 'button')
.attr('id', 'nrf-current-detail')
.addClass('nrf-top-button')
.text('詳情')
.on('click', function (e) {
e.preventDefault();
showCurrentDetail();
});
const $previewArea = $('<div>')
.attr('id', 'nrf-preview-area')
.addClass('nrf-preview-area')
.append(
$('<div>')
.addClass('nrf-empty-preview')
.text('請從左側選擇條目。')
);
$actions.append($addButton, $detailButton);
$content.append($title, $actions, $previewArea);
const grouped = {
'高需求': [],
'中需求': [],
'格式錯誤': []
};
candidates.forEach(function (item, index) {
item._nrfId = 'nrf-item-' + index;
item._editSummary = buildEditSummary(
item.advice.templateName,
item.advice.type,
item.referenceCount,
item.bodyLength,
item.hasReferenceHeading
);
if (!grouped[item.demand.group]) {
grouped[item.demand.group] = [];
}
grouped[item.demand.group].push(item);
});
['高需求', '中需求', '格式錯誤'].forEach(function (groupName) {
const groupItems = grouped[groupName];
if (!groupItems.length) {
return;
}
const $group = $('<div>').addClass('nrf-group');
const $groupTitle = $('<div>')
.addClass('nrf-group-title')
.text(groupName);
$group.append($groupTitle);
groupItems.forEach(function (item) {
const $item = $('<div>')
.attr('id', item._nrfId)
.addClass('nrf-list-item')
.on('click', function () {
selectItem(item);
});
const $text = $('<div>')
.addClass('nrf-list-item-text')
.text(item.title);
$item.append($text);
$group.append($item);
});
$sidebarList.append($group);
});
$sidebar.append($close, $sidebarList);
$window.append($sidebar, $content);
$overlay.append($window);
$('body').append($overlay);
if (candidates[0]) {
selectItem(candidates[0]);
}
mw.notify('发现 ' + candidates.length + ' 个候选条目。', {
title: TOOL_NAME,
type: 'success'
});
}
function closeNrfWindow() {
closeCurrentDetail(true);
const $overlay = $('#nrf-overlay');
if (!$overlay.length) {
return;
}
$overlay.addClass('nrf-closing');
window.setTimeout(function () {
$overlay.remove();
}, 190);
}
function selectItem(item) {
const $sidebarList = $('.nrf-sidebar-list');
$sidebarList.addClass('nrf-sidebar-changing');
window.setTimeout(function () {
nrfCurrentItem = item;
nrfCurrentItemId = item._nrfId;
$('.nrf-list-item').removeClass('nrf-list-item-selected');
$('#' + item._nrfId).addClass('nrf-list-item-selected');
$('#nrf-current-title').text(item.title);
const $addButton = $('#nrf-current-add');
if ($('#' + item._nrfId).hasClass('nrf-list-item-added')) {
$addButton
.prop('disabled', true)
.text('已加');
} else {
$addButton
.prop('disabled', false)
.text('添加');
}
showPreviewContent(item, false);
window.setTimeout(function () {
$sidebarList.removeClass('nrf-sidebar-changing');
}, 40);
}, 200);
}
function showPreviewContent(item, forceReload) {
const $preview = $('#nrf-preview-area');
if (!$preview.length) {
return;
}
$preview.removeClass('nrf-preview-scrolled');
const oldFrame = $preview.find('iframe.nrf-preview-frame');
oldFrame.addClass('nrf-preview-hidden');
const params = {};
if (forceReload) {
params.nrfreload = Date.now();
}
const previewUrl = mw.util.getUrl(item.title, params);
window.setTimeout(function () {
const $iframe = $('<iframe>')
.addClass('nrf-preview-frame nrf-preview-hidden')
.attr('title', '条目预览:' + item.title)
.attr('src', previewUrl)
.on('load', function () {
const iframe = this;
preparePreviewFrame(iframe);
bindPreviewScrollState(iframe);
window.setTimeout(function () {
$(iframe).removeClass('nrf-preview-hidden');
}, 30);
});
$preview.empty().append($iframe);
}, 200);
}
function preparePreviewFrame(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (!doc || !doc.head) {
return;
}
if (doc.getElementById('nrf-preview-frame-style')) {
return;
}
const style = doc.createElement('style');
style.id = 'nrf-preview-frame-style';
style.textContent = `
html,
body {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar,
*::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
header.vector-header.mw-header.no-font-mode-scale,
.vector-page-toolbar-container,
#vector-page-titlebar-toc,
span.mw-editsection,
#scrollDownButton-zhwiki,
#scrollUpButton-zhwiki,
#footer-places {
display: none !important;
}
`;
doc.head.appendChild(style);
} catch (e) {
console.warn('無法處理 iframe 內部樣式,可能受到同源策略限制。', e);
}
}
function bindPreviewScrollState(iframe) {
const $preview = $('#nrf-preview-area');
if (!$preview.length) {
return;
}
try {
const win = iframe.contentWindow;
const doc = iframe.contentDocument || win.document;
if (!win || !doc) {
$preview.removeClass('nrf-preview-scrolled');
return;
}
function updateGradient() {
const scrollTop =
win.scrollY ||
doc.documentElement.scrollTop ||
doc.body.scrollTop ||
0;
if (scrollTop > 6) {
$preview.addClass('nrf-preview-scrolled');
} else {
$preview.removeClass('nrf-preview-scrolled');
}
}
win.addEventListener('scroll', updateGradient, { passive: true });
updateGradient();
} catch (e) {
$preview.removeClass('nrf-preview-scrolled');
}
}
function showCurrentDetail() {
if (!nrfCurrentItem) {
mw.notify('尚未选择条目。', {
title: TOOL_NAME,
type: 'warn'
});
return;
}
closeCurrentDetail();
const item = nrfCurrentItem;
const $window = $('.nrf-window');
if (!$window.length) {
return;
}
const $layer = $('<div>')
.attr('id', 'nrf-detail-layer')
.addClass('nrf-detail-layer');
const $card = $('<div>')
.addClass('nrf-detail-card')
.on('click', function (e) {
e.stopPropagation();
});
const $title = $('<div>')
.addClass('nrf-detail-title')
.text(item.title);
const $subtitle = $('<div>')
.addClass('nrf-detail-subtitle')
.text(item.advice.reason);
const rows = [
{
label: '正文長度:',
value: item.bodyLength + ' 個字符'
},
{
label: '參考來源:',
value: item.referenceCount + ' 個'
},
{
label: '參考標題:',
value: item.hasReferenceHeading ? '是' : '否'
},
{
label: '使用模板:',
value: '{{' + item.advice.templateName + '}}'
},
{
label: '修改日志:',
value: item._editSummary
}
];
let $summaryValue = null;
rows.forEach(function (row, index) {
const rowClass = 'nrf-detail-row-' + (index + 1);
const isSummaryRow = row.label === '修改日志:';
const $label = $('<div>')
.addClass('nrf-detail-row-label')
.addClass(rowClass)
.text(row.label);
const $value = $('<div>')
.addClass('nrf-detail-row-value')
.addClass(rowClass)
.toggleClass('nrf-detail-summary', isSummaryRow)
.append(
$('<span>').text(row.value)
);
if (isSummaryRow) {
$summaryValue = $value;
}
$card.append($label, $value);
});
const $actions = $('<div>').addClass('nrf-detail-actions');
const $closeButton = $('<button>')
.attr('type', 'button')
.addClass('nrf-detail-button nrf-detail-close')
.text('關閉')
.on('click', function (e) {
e.preventDefault();
closeCurrentDetail();
});
const $addButton = $('<button>')
.attr('type', 'button')
.addClass('nrf-detail-button nrf-detail-add')
.text($('#' + item._nrfId).hasClass('nrf-list-item-added') ? '已加' : '添加')
.prop('disabled', $('#' + item._nrfId).hasClass('nrf-list-item-added'))
.on('click', function (e) {
e.preventDefault();
closeCurrentDetail();
addCurrentItem();
});
$actions.append($closeButton, $addButton);
$card.append($title, $subtitle, $actions);
$layer.append($card);
$window.addClass('nrf-detail-open').append($layer);
window.setTimeout(function () {
if (!$summaryValue || !$summaryValue.length) {
return;
}
const summaryTop = 248;
const summaryTextHeight = Math.max(24, $summaryValue.find('span')[0].scrollHeight);
const cardHeight = Math.max(330, summaryTop + summaryTextHeight + 64);
$summaryValue.css('height', summaryTextHeight + 'px');
$card.css('height', cardHeight + 'px');
}, 0);
}
function closeCurrentDetail(immediate) {
const $layer = $('#nrf-detail-layer');
if (!$layer.length) {
$('.nrf-window').removeClass('nrf-detail-open');
return;
}
if (immediate) {
$layer.remove();
$('.nrf-window').removeClass('nrf-detail-open');
return;
}
if ($layer.hasClass('nrf-detail-closing')) {
return;
}
$layer.addClass('nrf-detail-closing');
window.setTimeout(function () {
$layer.remove();
$('.nrf-window').removeClass('nrf-detail-open');
}, 190);
}
function addCurrentItem() {
if (!nrfCurrentItem) {
mw.notify('尚未选择条目。', {
title: TOOL_NAME,
type: 'warn'
});
return;
}
const item = nrfCurrentItem;
const $button = $('#nrf-current-add');
$button
.prop('disabled', true)
.text('添加中');
addTemplateToPage(
item.title,
item.advice.templateName,
item.advice.templateText,
item.advice.type,
item._nrfId,
$button
);
}
function addTemplateToPage(title, templateName, templateText, adviceType, itemId, $button) {
const api = new mw.Api();
api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: 'main',
formatversion: 2,
format: 'json'
}).done(function (data) {
const page = data.query.pages[0];
if (!page.revisions || !page.revisions[0]) {
mw.notify('无法读取页面内容:' + title, {
title: TOOL_NAME,
type: 'error'
});
markItemAsError(itemId, $button, '无法读取页面内容。');
return;
}
const oldText = page.revisions[0].slots.main.content;
const referenceCount = countReferences(oldText);
const bodyLength = countBodyTextLength(oldText);
const hasReferenceHeadingNow = hasReferencesHeading(oldText);
const advice = getTemplateAdvice(title, oldText, referenceCount, hasReferenceHeadingNow, bodyLength);
if (!advice.shouldTag) {
mw.notify('页面状态已变化,未添加模板:' + title, {
title: TOOL_NAME,
type: 'warn'
});
markItemAsSkipped(itemId, $button, '页面状态已变化,未添加模板。');
return;
}
const newText = templateText + oldText;
const dynamicSummary = buildEditSummary(
advice.templateName,
advice.type,
referenceCount,
bodyLength,
hasReferenceHeadingNow
);
api.postWithToken('csrf', {
action: 'edit',
title: title,
text: newText,
summary: dynamicSummary,
minor: true,
watchlist: 'nochange',
format: 'json'
}).done(function () {
markItemAsTagged(itemId, $button, templateName, dynamicSummary);
if (nrfCurrentItem && nrfCurrentItem.title === title) {
nrfCurrentItem.text = newText;
nrfCurrentItem.referenceCount = countReferences(newText);
nrfCurrentItem.bodyLength = countBodyTextLength(newText);
nrfCurrentItem.hasReferenceHeading = hasReferencesHeading(newText);
showPreviewContent(nrfCurrentItem, true);
}
mw.notify('已添加 {{' + templateName + '}}:' + title, {
title: TOOL_NAME,
type: 'success'
});
}).fail(function (code, result) {
console.error(code, result);
mw.notify('编辑失败:' + code, {
title: TOOL_NAME,
type: 'error'
});
markItemAsError(itemId, $button, '编辑失败:' + code);
});
}).fail(function (code, result) {
console.error(code, result);
mw.notify('读取页面内容失败:' + code, {
title: TOOL_NAME,
type: 'error'
});
markItemAsError(itemId, $button, '读取页面内容失败:' + code);
});
}
function markItemAsTagged(itemId, $button, templateName, dynamicSummary) {
const $item = $('#' + itemId);
$item
.addClass('nrf-list-item-added')
.css({
background: ''
});
if ($button && $button.length) {
$button
.prop('disabled', true)
.text('已加');
}
}
function markItemAsSkipped(itemId, $button, message) {
const $item = $('#' + itemId);
$item.css({
background: 'rgba(172, 102, 0, 0.16)'
});
if ($button && $button.length) {
$button
.prop('disabled', true)
.text('跳過');
}
}
function markItemAsError(itemId, $button, message) {
const $item = $('#' + itemId);
$item.css({
background: 'rgba(221, 51, 51, 0.16)'
});
if ($button && $button.length) {
$button
.prop('disabled', false)
.text('重試');
}
}
function getToday() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
})();