跳转到内容

User:PexEric/RefPageCheck.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ 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;
    }
`);