// ==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 = `
`;
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);
}
})();