User:PexEric/RefPageCheck.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
* 维基百科参考文献页码检查器
* 检查参考文献中引用的页码是否真实存在
*/
$(document).ready(() => {
if (mw.config.get('wgNamespaceNumber') === 0) {
mw.loader.using(
['mediawiki.util'],
function() {
var link = mw.util.addPortletLink('p-cactions', '#', '检查页码', 't-pagecheck', '检查参考文献页码是否存在');
$(link).click(function(event) {
event.preventDefault();
mw.loader.using('@wikimedia/codex', window.PageChecker.run);
});
}
);
}
});
// 主要功能模块
window.PageChecker = {
instance: null,
mountPoint: null,
run: function(require) {
const Vue = require('vue');
const Codex = require('@wikimedia/codex');
// 如果已经存在实例,先清理
if (window.PageChecker.instance) {
try {
window.PageChecker.instance.unmount();
} catch (e) {
// 忽略unmount错误
}
window.PageChecker.instance = null;
}
if (window.PageChecker.mountPoint) {
window.PageChecker.mountPoint.remove();
window.PageChecker.mountPoint = null;
}
const mountPoint = document.body.appendChild(document.createElement('div'));
window.PageChecker.mountPoint = mountPoint;
const app = Vue.createMwApp({
data: function() {
return {
showDialog: false,
isLoading: false,
results: [],
error: null,
uncheckedRefs: [],
primaryAction: {
label: '关闭',
actionType: 'progressive'
},
secondaryAction: {
label: '复制',
actionType: 'normal'
}
};
},
template: `
<cdx-dialog
v-model:open="showDialog"
:title="dialogTitle"
:use-close-button="true"
:primary-action="primaryAction"
:default-action="secondaryAction"
@primary="showDialog = false"
@default="copyResults"
>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在检查页码,请稍候...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<cdx-message type="error" :fade-in="true">
<strong>检查出错:</strong>{{ error }}
</cdx-message>
</div>
<!-- 结果显示 -->
<div v-else-if="results.length > 0 || uncheckedRefs.length > 0">
<div v-if="results.length > 0" class="results-container">
<div v-for="result in results" :key="getResultKey(result)" class="result-item">
<div v-html="formatResult(result)"></div>
</div>
</div>
<!-- 未检查的角标 -->
<div v-if="uncheckedRefs.length > 0" class="unchecked-refs">
<span class="unchecked-label">未检查:</span>
<span class="unchecked-list">{{ formatUncheckedRefs() }}</span>
</div>
</div>
<!-- 无结果 -->
<div v-else class="no-results">
<cdx-message type="notice" :fade-in="true">
未找到包含页码的参考文献
</cdx-message>
</div>
</cdx-dialog>
`,
computed: {
dialogTitle() {
if (this.isLoading) return '正在检查页码';
if (this.error) return '检查出错';
return '页码检查结果';
}
},
methods: {
async openDialog() {
this.showDialog = true;
this.isLoading = true;
this.error = null;
this.results = [];
this.uncheckedRefs = [];
try {
await this.checkPageNumbers();
} catch (error) {
console.error('检查过程中出错:', error);
this.error = error.message;
} finally {
this.isLoading = false;
}
},
async checkPageNumbers() {
const results = [];
const processedRefs = new Set();
const harvardRefNumbers = new Set();
const checkedRefNumbers = new Set();
// 先收集哈佛引用使用的角标号
const harvardRefs = document.querySelectorAll('span.reference-text a[href^="#CITEREF"]');
for (const link of harvardRefs) {
const refNumber = this.getRefNumber(link);
if (refNumber) {
harvardRefNumbers.add(refNumber);
}
}
// 1. 处理普通ref引用(排除哈佛引用)
const refs = document.querySelectorAll('ol.references li');
for (let i = 0; i < refs.length; i++) {
const ref = refs[i];
const refId = ref.id;
const refNumber = i + 1;
if (harvardRefNumbers.has(refNumber)) continue;
if (processedRefs.has(refId)) continue;
processedRefs.add(refId);
const hasPages = await this.processRegularRef(ref, refNumber, results);
if (hasPages) {
checkedRefNumbers.add(refNumber);
}
}
// 2. 处理哈佛式引用
const harvardResults = await this.processHarvardRefs(results);
harvardResults.forEach(refNum => checkedRefNumbers.add(refNum));
// 3. 收集未检查的角标
const allRefNumbers = [];
for (let i = 1; i <= refs.length; i++) {
allRefNumbers.push(i);
}
this.uncheckedRefs = allRefNumbers.filter(num => !checkedRefNumbers.has(num));
this.results = results;
},
async processRegularRef(refElement, refNumber, results) {
const cites = refElement.querySelectorAll('cite.citation');
const rpPages = this.getRpPages(refElement);
if (cites.length === 0) return false;
let hasPages = false;
if (cites.length === 1) {
const cite = cites[0];
const isbn = this.extractISBN(cite);
const selfPages = this.extractPagesFromCite(cite);
const allPages = this.expandPageRanges([...selfPages, ...rpPages]);
if (selfPages.length > 0 || rpPages.length > 0) {
hasPages = true;
const bookResult = isbn ? await this.getBookInfo(isbn) : { totalPages: null, error: '无ISBN' };
results.push({
type: 'regular',
number: refNumber,
isbn: isbn,
totalPages: bookResult.totalPages,
error: bookResult.error,
pages: {
self: selfPages,
rp: rpPages
},
status: this.getStatus(allPages, bookResult.totalPages, bookResult.error)
});
}
} else {
const citeResults = [];
for (let j = 0; j < cites.length; j++) {
const cite = cites[j];
const isbn = this.extractISBN(cite);
const selfPages = this.extractPagesFromCite(cite);
if (selfPages.length > 0) {
hasPages = true;
const bookResult = isbn ? await this.getBookInfo(isbn) : { totalPages: null, error: '无ISBN' };
const allPages = this.expandPageRanges(selfPages);
citeResults.push({
isbn: isbn,
totalPages: bookResult.totalPages,
error: bookResult.error,
pages: selfPages,
status: this.getStatus(allPages, bookResult.totalPages, bookResult.error)
});
}
}
if (citeResults.length > 0) {
results.push({
type: 'multiple',
number: refNumber,
cites: citeResults
});
}
}
return hasPages;
},
async processHarvardRefs(results) {
const harvardRefs = document.querySelectorAll('span.reference-text a[href^="#CITEREF"]');
const processedHarvard = new Map();
const checkedRefNumbers = [];
for (const link of harvardRefs) {
const href = link.getAttribute('href');
const refName = link.textContent.trim();
const pageText = link.parentElement.textContent;
const pages = this.extractHarvardPages(pageText);
const refNumber = this.getRefNumber(link);
if (!processedHarvard.has(refName)) {
processedHarvard.set(refName, {
name: refName,
href: href,
pages: [],
refNumbers: []
});
}
const harvardData = processedHarvard.get(refName);
harvardData.pages.push(...pages.map(p => ({ page: p, refNumber })));
if (refNumber) {
harvardData.refNumbers.push(refNumber);
checkedRefNumbers.push(refNumber);
}
}
for (const [refName, data] of processedHarvard) {
const targetId = data.href.substring(1);
const bibEntry = document.getElementById(targetId);
if (bibEntry) {
const isbn = this.extractISBN(bibEntry);
const selfPages = this.extractPagesFromCite(bibEntry);
const bookResult = isbn ? await this.getBookInfo(isbn) : { totalPages: null, error: '无ISBN' };
const allPageRanges = [...selfPages, ...data.pages.map(p => p.page)];
const allPages = this.expandPageRanges(allPageRanges);
results.push({
type: 'harvard',
name: refName,
isbn: isbn,
totalPages: bookResult.totalPages,
error: bookResult.error,
pages: {
self: selfPages,
harvard: data.pages
},
status: this.getStatus(allPages, bookResult.totalPages, bookResult.error)
});
}
}
return checkedRefNumbers;
},
extractISBN(element) {
const isbnLink = element.querySelector('a[href*="BookSources"]');
if (isbnLink) {
const match = isbnLink.href.match(/BookSources\/([0-9\-X]+)/);
return match ? match[1].replace(/-/g, '') : null;
}
return null;
},
extractPagesFromCite(cite) {
const text = cite.textContent;
const pages = [];
const pageMatch = text.match(/:\s*([0-9\-–—,,、\s]+)[\.\s]/);
if (pageMatch) {
pages.push(...this.parsePageRangeKeepFormat(pageMatch[1]));
}
return pages;
},
getRpPages(refElement) {
const rpElement = refElement.nextElementSibling;
if (rpElement && rpElement.classList.contains('reference')) {
const rpText = rpElement.textContent;
const match = rpText.match(/((?:pp?\.\s*)?([^)]+))/);
if (match) {
return this.parsePageRangeKeepFormat(match[1]);
}
}
return [];
},
extractHarvardPages(text) {
const match = text.match(/第([0-9\-–—,,、\s]+)[页頁]/);
return match ? this.parsePageRangeKeepFormat(match[1]) : [];
},
// 保持原始格式的页码解析
parsePageRangeKeepFormat(pageStr) {
const parts = pageStr.split(/[,,、]/).map(p => p.trim()).filter(p => p);
return parts;
},
// 展开页码范围用于验证
expandPageRanges(pageRanges) {
const pages = [];
for (const range of pageRanges) {
const rangeMatch = range.match(/^(\d+)[\-–—](\d+)$/);
if (rangeMatch) {
const start = parseInt(rangeMatch[1]);
const end = parseInt(rangeMatch[2]);
for (let i = start; i <= end; i++) {
pages.push(i);
}
} else {
const pageNum = parseInt(range);
if (!isNaN(pageNum)) {
pages.push(pageNum);
}
}
}
return pages;
},
getRefNumber(element) {
const refElement = element.closest('li');
if (refElement) {
const refList = refElement.parentElement;
return Array.from(refList.children).indexOf(refElement) + 1;
}
return null;
},
async getBookInfo(isbn) {
try {
const response = await fetch(`https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`);
const data = await response.json();
if (data.items && data.items.length > 0) {
const book = data.items[0].volumeInfo;
return {
totalPages: book.pageCount || null,
error: book.pageCount ? null : 'API未返回页数信息'
};
} else {
return { totalPages: null, error: 'API未找到该ISBN对应的图书' };
}
} catch (error) {
return { totalPages: null, error: `API请求失败: ${error.message}` };
}
},
getStatus(pages, totalPages, error) {
if (!totalPages) {
// API未返回页数信息显示红色问号
return error === 'API未返回页数信息' ? 'error-unknown' : 'unknown';
}
const maxPage = Math.max(...pages);
return maxPage <= totalPages ? 'success' : 'error';
},
getResultKey(result) {
if (result.type === 'harvard') {
return `harvard-${result.name}`;
}
return `${result.type}-${result.number}`;
},
formatResult(result) {
const getStatusIcon = (status, error) => {
if (status === 'success') return '<span class="status-success">✓</span>';
if (status === 'error') return '<span class="status-error">✗</span>';
if (status === 'error-unknown') return `<span class="status-error-unknown" title="${error || '无法获取图书信息'}">?</span>`;
return `<span class="status-unknown" title="${error || '无法获取图书信息'}">?</span>`;
};
if (result.type === 'regular') {
const statusIcon = getStatusIcon(result.status, result.error);
const totalPagesText = result.totalPages ? `(${result.totalPages})` : '';
let content = `• ${statusIcon} [${result.number}]${totalPagesText}:`;
const pageTexts = [];
if (result.pages.self.length > 0) {
pageTexts.push(result.pages.self.join('、') + '<sup>self</sup>');
}
if (result.pages.rp.length > 0) {
pageTexts.push(result.pages.rp.join('、') + '<sup>rp</sup>');
}
return content + pageTexts.join('、');
} else if (result.type === 'multiple') {
let content = `• [${result.number}]:`;
const citeTexts = [];
result.cites.forEach((cite, index) => {
const statusIcon = getStatusIcon(cite.status, cite.error);
const totalPagesText = cite.totalPages ? `(${cite.totalPages})` : '';
citeTexts.push(`${statusIcon} cite${index + 1}${totalPagesText}:${cite.pages.join('、')}`);
});
return content + citeTexts.join(';');
} else if (result.type === 'harvard') {
const statusIcon = getStatusIcon(result.status, result.error);
const totalPagesText = result.totalPages ? `(${result.totalPages})` : '';
let content = `• ${statusIcon} ${this.escapeHtml(result.name)}${totalPagesText}:`;
const pageTexts = [];
if (result.pages.self.length > 0) {
pageTexts.push(result.pages.self.join('、') + '<sup>self</sup>');
}
result.pages.harvard.forEach(p => {
pageTexts.push(`${p.page}<sup>[${p.refNumber}]</sup>`);
});
return content + pageTexts.join('、');
}
return '';
},
// 格式化未检查的角标
formatUncheckedRefs() {
if (this.uncheckedRefs.length === 0) return '';
// 将连续的数字合并为范围
const ranges = [];
let start = this.uncheckedRefs[0];
let end = start;
for (let i = 1; i < this.uncheckedRefs.length; i++) {
if (this.uncheckedRefs[i] === end + 1) {
end = this.uncheckedRefs[i];
} else {
if (start === end) {
ranges.push(`[${start}]`);
} else if (end === start + 1) {
ranges.push(`[${start}]、[${end}]`);
} else {
ranges.push(`[${start}]–[${end}]`);
}
start = this.uncheckedRefs[i];
end = start;
}
}
// 处理最后一个范围
if (start === end) {
ranges.push(`[${start}]`);
} else if (end === start + 1) {
ranges.push(`[${start}]、[${end}]`);
} else {
ranges.push(`[${start}]–[${end}]`);
}
return ranges.join('、');
},
// 复制结果到剪贴板
copyResults() {
const getStatusText = (status) => {
if (status === 'success') return '{{Y}}';
if (status === 'error' || status === 'error-unknown') return '{{N}}';
return '{{?}}';
};
let text = '';
for (const result of this.results) {
if (result.type === 'regular') {
const statusText = getStatusText(result.status);
const totalPagesText = result.totalPages ? `(${result.totalPages})` : '';
text += `* ${statusText} [${result.number}]${totalPagesText}:`;
const pageTexts = [];
if (result.pages.self.length > 0) {
pageTexts.push(result.pages.self.join('、') + '<sup>self</sup>');
}
if (result.pages.rp.length > 0) {
pageTexts.push(result.pages.rp.join('、') + '<sup>rp</sup>');
}
text += pageTexts.join('、') + '\n';
} else if (result.type === 'multiple') {
text += `* [${result.number}]:`;
const citeTexts = [];
result.cites.forEach((cite, index) => {
const statusText = getStatusText(cite.status);
const totalPagesText = cite.totalPages ? `(${cite.totalPages})` : '';
citeTexts.push(`${statusText} cite${index + 1}${totalPagesText}:${cite.pages.join('、')}`);
});
text += citeTexts.join(';') + '\n';
} else if (result.type === 'harvard') {
const statusText = getStatusText(result.status);
const totalPagesText = result.totalPages ? `(${result.totalPages})` : '';
text += `* ${statusText} ${result.name}${totalPagesText}:`;
const pageTexts = [];
if (result.pages.self.length > 0) {
pageTexts.push(result.pages.self.join('、') + '<sup>self</sup>');
}
result.pages.harvard.forEach(p => {
pageTexts.push(`${p.page}<sup>[${p.refNumber}]</sup>`);
});
text += pageTexts.join('、') + '\n';
}
}
// 添加未检查的角标
if (this.uncheckedRefs.length > 0) {
text += `未检查:${this.formatUncheckedRefs()}\n`;
}
// 复制到剪贴板
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
mw.notify('结果已复制到剪贴板', { type: 'success' });
}).catch(() => {
this.fallbackCopy(text);
});
} else {
this.fallbackCopy(text);
}
},
// 备用复制方法
fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
mw.notify('结果已复制到剪贴板', { type: 'success' });
} catch (err) {
mw.notify('复制失败,请手动复制', { type: 'error' });
}
document.body.removeChild(textArea);
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
},
mounted() {
this.openDialog();
}
})
.component('cdx-dialog', Codex.CdxDialog)
.component('cdx-message', Codex.CdxMessage);
window.PageChecker.instance = app.mount(mountPoint);
}
};
// 添加CSS样式
mw.util.addCSS(`
.loading-container {
text-align: center;
padding: 40px 20px;
}
.loading-spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid #eaecf0;
border-top: 3px solid #0645ad;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results-container {
max-height: 500px;
overflow-y: auto;
padding: 8px 0;
}
.result-item {
margin-bottom: 8px;
font-family: monospace;
font-size: 14px;
line-height: 1.5;
}
.result-item sup {
font-size: 11px;
color: #0645ad;
}
.error-container,
.no-results {
padding: 20px;
}
/* 未检查角标样式 */
.unchecked-refs {
margin-top: 16px;
padding: 12px;
border-top: 1px solid #eaecf0;
font-size: 13px;
color: #72777d;
font-family: monospace;
}
.unchecked-label {
font-weight: 500;
}
.unchecked-list {
margin-left: 8px;
}
/* 状态图标颜色 */
.status-success {
color: #00af89;
font-weight: bold;
}
.status-error {
color: #d33;
font-weight: bold;
}
.status-error-unknown {
color: #d33;
font-weight: bold;
cursor: help;
}
.status-unknown {
color: #fc3;
font-weight: bold;
cursor: help;
}
`);