// ==UserScript== // @name WebCite 网页参考文献助手 // @name:en WebCite Web Citation Helper // @namespace https://github.com/SZC/WebCite // @version 1.1.2 // @description 一键抓取网页信息并生成 APA 7th、GB/T 7714-2015 或 IEEE 参考文献。 // @description:en Capture web page metadata and generate APA 7th, GB/T 7714-2015, or IEEE references. // @author SZC // @license MIT // @icon data:image/png;base64,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 // @match http://*/* // @match https://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @grant GM_registerMenuCommand // @run-at document-end // @noframes // @downloadURL https://update.greasyfork.icu/scripts/575673/WebCite%20%E7%BD%91%E9%A1%B5%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/575673/WebCite%20%E7%BD%91%E9%A1%B5%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function () { "use strict"; if (window.top !== window.self) { return; } const APP_VERSION = "V1.1.2"; const APP_DEVELOPER = "SZC"; const APP_ICON_DATA_URL = "data:image/png;base64,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"; const STORAGE_KEY = "webcite-userscript-state"; const POSITION_KEY = "webcite-userscript-position"; const HOST_ID = "webcite-userscript-root"; const EDGE_PADDING = 10; const DEFAULT_STATE = { citationStyle: "apa", activeListId: "default", lists: [ { id: "default", name: "默认列表", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), citations: [] } ] }; if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand("显示 WebCite 悬浮窗", () => { mountFloatingWindow(); forceShowFloatingWindow(); }); GM_registerMenuCommand("打开 WebCite 管理面板", openManager); GM_registerMenuCommand("添加当前网页到引用列表", addCurrentPage); GM_registerMenuCommand("WebCite 诊断信息", showDiagnostics); } safeStart(); function safeStart() { try { console.info("[WebCite] 脚本开始运行", { version: APP_VERSION, href: location.href, readyState: document.readyState, hasBody: Boolean(document.body) }); waitForBody(mountFloatingWindow); } catch (error) { console.error("[WebCite] 启动失败", error); } } function mountFloatingWindow() { if (document.getElementById(HOST_ID)) { console.info("[WebCite] 悬浮窗已存在"); return; } const host = document.createElement("div"); host.id = HOST_ID; Object.assign(host.style, { position: "fixed", right: "18px", bottom: "22px", zIndex: "2147483647" }); const shadow = host.attachShadow({ mode: "open" }); shadow.innerHTML = `
WebCite
`; document.body.appendChild(host); restorePosition(host); enableDragging(host, shadow.getElementById("drag-handle")); shadow.getElementById("add-button").addEventListener("click", addCurrentPage); shadow.getElementById("copy-button").addEventListener("click", copyCurrentPage); shadow.getElementById("settings-button").addEventListener("click", openManager); shadow.getElementById("close-manager").addEventListener("click", closeManager); shadow.getElementById("manager-backdrop").addEventListener("click", (event) => { if (event.target === shadow.getElementById("manager-backdrop")) { closeManager(); } }); console.info("[WebCite] 悬浮窗已加载", APP_VERSION); } function forceShowFloatingWindow() { const host = document.getElementById(HOST_ID); if (!host) { console.warn("[WebCite] 强制显示失败:找不到悬浮窗根节点"); return; } Object.assign(host.style, { display: "block", visibility: "visible", opacity: "1", position: "fixed", left: "auto", top: "auto", right: "18px", bottom: "22px", zIndex: "2147483647" }); console.info("[WebCite] 已强制显示悬浮窗"); } function showDiagnostics() { const host = document.getElementById(HOST_ID); const info = { version: APP_VERSION, href: location.href, readyState: document.readyState, hasBody: Boolean(document.body), hostExists: Boolean(host), hostRect: host ? host.getBoundingClientRect().toJSON?.() || String(host.getBoundingClientRect()) : null, hostStyle: host ? host.getAttribute("style") : null, shadowExists: Boolean(host?.shadowRoot), storageWorks: testStorage() }; console.info("[WebCite] 诊断信息", info); alert( [ `WebCite ${APP_VERSION}`, `脚本页面: ${location.href}`, `body: ${info.hasBody ? "yes" : "no"}`, `悬浮窗节点: ${info.hostExists ? "yes" : "no"}`, `Shadow DOM: ${info.shadowExists ? "yes" : "no"}`, `存储: ${info.storageWorks ? "ok" : "failed"}`, "详细信息已输出到控制台 Console" ].join("\n") ); } function testStorage() { try { setValue("webcite-diagnostic-test", { ok: true, at: Date.now() }); return Boolean(getValue("webcite-diagnostic-test", null)?.ok); } catch (error) { console.error("[WebCite] 存储诊断失败", error); return false; } } function waitForBody(callback) { if (document.body) { callback(); return; } let attempts = 0; const timer = window.setInterval(() => { attempts += 1; if (document.body) { window.clearInterval(timer); callback(); } if (attempts > 100) { window.clearInterval(timer); console.warn("[WebCite] 页面 body 未就绪,悬浮窗未加载"); } }, 50); } function enableDragging(host, handle) { let drag = null; handle.addEventListener("pointerdown", (event) => { const rect = host.getBoundingClientRect(); drag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startLeft: rect.left, startTop: rect.top }; host.classList.add("dragging"); handle.setPointerCapture(event.pointerId); event.preventDefault(); }); handle.addEventListener("pointermove", (event) => { if (!drag || drag.pointerId !== event.pointerId) { return; } const nextLeft = drag.startLeft + event.clientX - drag.startX; const nextTop = drag.startTop + event.clientY - drag.startY; applyPosition(host, clampPosition(host, nextLeft, nextTop)); }); const stopDragging = (event) => { if (!drag || drag.pointerId !== event.pointerId) { return; } drag = null; host.classList.remove("dragging"); const rect = host.getBoundingClientRect(); setValue(POSITION_KEY, { left: rect.left, top: rect.top }); }; handle.addEventListener("pointerup", stopDragging); handle.addEventListener("pointercancel", stopDragging); } function restorePosition(host) { const position = getValue(POSITION_KEY, null); if (position && typeof position.left === "number" && typeof position.top === "number") { applyPosition(host, clampPosition(host, position.left, position.top)); } } function applyPosition(host, position) { host.style.left = `${position.left}px`; host.style.top = `${position.top}px`; host.style.right = "auto"; host.style.bottom = "auto"; } function clampPosition(host, left, top) { const rect = host.getBoundingClientRect(); return { left: clamp(left, EDGE_PADDING, Math.max(EDGE_PADDING, window.innerWidth - rect.width - EDGE_PADDING)), top: clamp(top, EDGE_PADDING, Math.max(EDGE_PADDING, window.innerHeight - rect.height - EDGE_PADDING)) }; } function addCurrentPage() { const state = getState(); const activeList = getActiveList(state); activeList.citations.push(extractCitation()); activeList.updatedAt = new Date().toISOString(); saveState(state); showToast(`已添加到「${activeList.name}」`); renderManagerIfOpen(); } function copyCurrentPage() { const state = getState(); const activeList = getActiveList(state); const citation = extractCitation(); const text = formatCitation(citation, state.citationStyle, activeList.citations.length + 1); writeClipboard(text); showToast("引用已复制"); } function openManager() { const shadow = document.getElementById(HOST_ID).shadowRoot; renderManager(); shadow.getElementById("manager-backdrop").classList.add("show"); } function closeManager() { const shadow = document.getElementById(HOST_ID).shadowRoot; shadow.getElementById("manager-backdrop").classList.remove("show"); } function renderManagerIfOpen() { const shadow = document.getElementById(HOST_ID).shadowRoot; if (shadow.getElementById("manager-backdrop").classList.contains("show")) { renderManager(); } } function renderManager() { const shadow = document.getElementById(HOST_ID).shadowRoot; const body = shadow.getElementById("manager-body"); const state = getState(); const activeList = getActiveList(state); const formattedList = formatCitationList(activeList, state.citationStyle); body.innerHTML = `
${ activeList.citations.length ? activeList.citations .map( (citation, index) => `

${escapeHtml(formatCitation(citation, state.citationStyle, index + 1))}

` ) .join("") : '
暂无引用
' }
`; body.querySelector("#style-select").value = state.citationStyle; body.querySelector("#list-select").value = state.activeListId; body.querySelector("#style-select").addEventListener("change", (event) => { state.citationStyle = event.target.value; saveState(state); renderManager(); }); body.querySelector("#list-select").addEventListener("change", (event) => { state.activeListId = event.target.value; saveState(state); renderManager(); }); body.querySelector("#rename-list").addEventListener("click", () => { activeList.name = body.querySelector("#rename-input").value.trim() || activeList.name; activeList.updatedAt = new Date().toISOString(); saveState(state); renderManager(); }); body.querySelector("#delete-list").addEventListener("click", () => { if (!confirm(`删除引用列表「${activeList.name}」?`)) { return; } state.lists = state.lists.filter((list) => list.id !== activeList.id); if (state.lists.length === 0) { state.lists = clone(DEFAULT_STATE.lists); } state.activeListId = state.lists[0].id; saveState(state); renderManager(); }); body.querySelector("#create-list").addEventListener("click", () => { const name = body.querySelector("#new-list-name").value.trim() || "新引用列表"; const list = { id: createId("list"), name, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), citations: [] }; state.lists.push(list); state.activeListId = list.id; saveState(state); renderManager(); }); body.querySelector("#copy-list").addEventListener("click", () => { writeClipboard(formattedList); showToast("列表引用已复制"); }); body.querySelector("#export-txt").addEventListener("click", () => { downloadFile(`${activeList.name}-${state.citationStyle}.txt`, `${formattedList}\n`, "text/plain;charset=utf-8"); }); body.querySelector("#export-md").addEventListener("click", () => { downloadFile(`${activeList.name}-${state.citationStyle}.md`, `# ${activeList.name}\n\n${formattedList}\n`, "text/markdown;charset=utf-8"); }); body.querySelector("#export-json").addEventListener("click", () => { downloadFile( `${activeList.name}-${state.citationStyle}.json`, JSON.stringify({ list: activeList, citationStyle: state.citationStyle }, null, 2), "application/json;charset=utf-8" ); }); body.querySelector("#clear-list").addEventListener("click", () => { if (!activeList.citations.length || !confirm(`清空「${activeList.name}」中的全部引用?`)) { return; } activeList.citations = []; activeList.updatedAt = new Date().toISOString(); saveState(state); renderManager(); }); body.querySelectorAll("[data-action]").forEach((button) => { button.addEventListener("click", () => { handleCitationAction(button.dataset.action, button.dataset.id); }); }); } function handleCitationAction(action, citationId) { const state = getState(); const activeList = getActiveList(state); const citation = activeList.citations.find((item) => item.id === citationId); if (!citation) { return; } if (action === "copy") { const index = activeList.citations.indexOf(citation) + 1; writeClipboard(formatCitation(citation, state.citationStyle, index)); showToast("单条引用已复制"); return; } if (action === "delete") { activeList.citations = activeList.citations.filter((item) => item.id !== citationId); activeList.updatedAt = new Date().toISOString(); saveState(state); renderManager(); return; } if (action === "edit") { citation.title = prompt("标题", citation.title) || citation.title; citation.author = prompt("作者", citation.author) || citation.author; citation.siteName = prompt("网站名称", citation.siteName) || citation.siteName; citation.publishedDate = prompt("发布日期 YYYY-MM-DD,可留空", citation.publishedDate || "") || undefined; citation.accessDate = prompt("引用日期 YYYY-MM-DD", citation.accessDate) || citation.accessDate; citation.url = prompt("URL", citation.url) || citation.url; citation.updatedAt = new Date().toISOString(); activeList.updatedAt = new Date().toISOString(); saveState(state); renderManager(); } } function extractCitation() { const siteName = extractSiteName(); const now = new Date(); return { id: createId("citation"), title: cleanText(selectText("h1")) || cleanText(selectAttr('meta[property="og:title"]', "content")) || cleanText(document.title) || location.href, author: cleanAuthor(selectAttr('meta[name="author"]', "content")) || cleanAuthor(selectAttr('meta[property="article:author"]', "content")) || cleanAuthor(selectText(".author")) || cleanAuthor(selectText(".byline")) || siteName, siteName, url: location.href, publishedDate: normalizeDate( selectAttr('meta[name="pubdate"]', "content") || selectAttr('meta[name="date"]', "content") || selectAttr('meta[property="article:published_time"]', "content") || selectAttr("time[datetime]", "datetime") || selectText("time") ), accessDate: toIsoDate(now), addedAt: now.toISOString(), updatedAt: now.toISOString() }; } function extractSiteName() { const siteName = cleanText(selectAttr('meta[property="og:site_name"]', "content")); if (siteName) { return siteName; } return location.hostname.replace(/^www\./, "") || "Web"; } function formatCitation(citation, style, index) { if (style === "gbt7714") { return compactSpaces( `[${index}] ${citation.author || citation.siteName || "佚名"}. ${citation.title || "Untitled"}[EB/OL]. ${ citation.publishedDate ? `(${citation.publishedDate})` : "" }[${citation.accessDate}]. ${citation.url}.` ); } if (style === "ieee") { const published = formatIeeeMonthYear(citation.publishedDate); const accessed = formatIeeeMonthYear(citation.accessDate); return compactSpaces( `[${index}] ${formatIeeeAuthor(citation.author || citation.siteName || "Unknown")}, “${ citation.title || "Untitled" },” ${citation.siteName || "Website"}, ${citation.url}${published ? `, ${published}` : ""}${ accessed ? ` (accessed ${accessed})` : "" }.` ); } const sitePart = citation.siteName && citation.siteName !== citation.author ? ` ${citation.siteName}.` : ""; return compactSpaces( `${citation.author || citation.siteName || "佚名"}. (${formatApaDate(citation.publishedDate)}). ${ citation.title || "Untitled" }.${sitePart} ${citation.url}` ); } function formatCitationList(list, style) { return list.citations.map((citation, index) => formatCitation(citation, style, index + 1)).join("\n"); } function getState() { const state = getValue(STORAGE_KEY, null); if (!state || !Array.isArray(state.lists) || state.lists.length === 0) { return clone(DEFAULT_STATE); } if (!state.lists.some((list) => list.id === state.activeListId)) { state.activeListId = state.lists[0].id; } if (!["apa", "gbt7714", "ieee"].includes(state.citationStyle)) { state.citationStyle = "apa"; } return state; } function saveState(state) { setValue(STORAGE_KEY, state); } function getActiveList(state) { return state.lists.find((list) => list.id === state.activeListId) || state.lists[0]; } function getValue(key, fallback) { if (typeof GM_getValue === "function") { return GM_getValue(key, fallback); } const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } function setValue(key, value) { if (typeof GM_setValue === "function") { GM_setValue(key, value); return; } localStorage.setItem(key, JSON.stringify(value)); } function writeClipboard(text) { if (typeof GM_setClipboard === "function") { GM_setClipboard(text); return; } navigator.clipboard.writeText(text); } function downloadFile(filename, content, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = safeFileName(filename); link.click(); URL.revokeObjectURL(url); } function selectText(selector) { return document.querySelector(selector)?.textContent || ""; } function selectAttr(selector, attr) { return document.querySelector(selector)?.getAttribute(attr) || ""; } function cleanText(value) { return String(value || "").replace(/\s+/g, " ").trim(); } function cleanAuthor(value) { return cleanText(value).replace(/^by\s+/i, "").replace(/^作者[::]\s*/, ""); } function normalizeDate(value) { const text = cleanText(value); if (!text) { return undefined; } const chineseDate = text.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日?/); if (chineseDate) { return buildIsoDate(chineseDate[1], chineseDate[2], chineseDate[3]); } const numericDate = text.match(/(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})/); if (numericDate) { return buildIsoDate(numericDate[1], numericDate[2], numericDate[3]); } const parsed = Date.parse(text); return Number.isNaN(parsed) ? undefined : toIsoDate(new Date(parsed)); } function buildIsoDate(year, month, day) { return `${year}-${String(Number(month)).padStart(2, "0")}-${String(Number(day)).padStart(2, "0")}`; } function toIsoDate(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function formatApaDate(value) { if (!value) { return "n.d."; } const date = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!date) { return value; } return `${date[1]}, ${monthName(Number(date[2]) - 1, false)} ${Number(date[3])}`; } function formatIeeeMonthYear(value) { const date = String(value || "").match(/^(\d{4})-(\d{2})(?:-\d{2})?$/); if (!date) { return value || ""; } return `${monthName(Number(date[2]) - 1, true)} ${date[1]}`; } function monthName(index, ieee) { const months = ieee ? ["Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."] : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; return months[index] || ""; } function formatIeeeAuthor(author) { const clean = cleanText(author); if (!clean || /[\u3400-\u9fff]/.test(clean) || /^[A-Z]\.\s+/.test(clean)) { return clean; } const parts = clean.split(/\s+/); if (parts.length < 2 || parts.some((part) => part.length === 1)) { return clean; } return `${parts.slice(0, -1).map((name) => `${name[0].toUpperCase()}.`).join(" ")} ${parts.at(-1)}`; } function createId(prefix) { if (crypto.randomUUID) { return `${prefix}_${crypto.randomUUID()}`; } return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; } function showToast(message) { const shadow = document.getElementById(HOST_ID).shadowRoot; const toast = shadow.getElementById("toast"); toast.textContent = message; toast.classList.add("show"); window.clearTimeout(showToast.timer); showToast.timer = window.setTimeout(() => toast.classList.remove("show"), 1800); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function compactSpaces(value) { return value.replace(/\s+/g, " ").replace(/\s+\./g, ".").trim(); } function safeFileName(value) { return value.replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_"); } function clone(value) { return JSON.parse(JSON.stringify(value)); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } })();