From d595db7a603f8cbba805a8c3041a92fa0787b765 Mon Sep 17 00:00:00 2001
From: edgerhao <112294576+edgerhao@users.noreply.github.com>
Date: Tue, 6 Jan 2026 22:43:01 +0800
Subject: [PATCH 1/2] feat: update plugins and data
---
.../plugins/pandoc-live-preview/README.md | 148 +++++
.../plugins/pandoc-live-preview/data.json | 16 +
.../plugins/pandoc-live-preview/main.js | 544 ++++++++++++++++++
.../plugins/pandoc-live-preview/manifest.json | 10 +
.../plugins/pandoc-live-preview/styles.css | 115 ++++
.../.obsidian/plugins/paperbell/data.json | 6 +-
6 files changed, 836 insertions(+), 3 deletions(-)
create mode 100644 PaperBell/.obsidian/plugins/pandoc-live-preview/README.md
create mode 100644 PaperBell/.obsidian/plugins/pandoc-live-preview/data.json
create mode 100644 PaperBell/.obsidian/plugins/pandoc-live-preview/main.js
create mode 100644 PaperBell/.obsidian/plugins/pandoc-live-preview/manifest.json
create mode 100644 PaperBell/.obsidian/plugins/pandoc-live-preview/styles.css
diff --git a/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md b/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md
new file mode 100644
index 00000000..95d6d211
--- /dev/null
+++ b/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md
@@ -0,0 +1,148 @@
+# Pandoc Live Preview for Obsidian
+
+[中文说明](#中文说明) | [Report Bug](https://github.com/wanxinhao/pandoc-live-preview/issues)
+
+This is an Obsidian plugin that provides **real-time preview** for Pandoc citations and cross-references. It is specifically optimized for **Academic Writing** workflows involving Pandoc and CJK (Chinese/Japanese/Korean) layouts.
+
+Now with **PicGo Integration**, it streamlines the entire "Paste -> Upload -> Cite" workflow!
+
+## ✨ Features
+
+- **Real-time Rendering**: Instantly turns `@fig:id` into readable labels like **图1** (Figure 1) or **表1** (Table 1) in Live Preview mode.
+- **🖼️ Auto Image Upload (PicGo)**:
+ - Paste an image, and it automatically uploads to your **PicGo server**.
+ - Automatically appends a unique ID: `{#fig:2025...}`.
+ - Option to **delete the local file** after successful upload to keep your vault clean.
+- **⚡ Smart Autocomplete**: Type `@` to trigger a suggestion menu of all figures and tables in your document. No need to memorize long IDs!
+- **⚡ Quick ID Generation**: Use commands to insert unique, timestamp-based IDs (e.g., `{#fig:202501011200}`) instantly.
+- **Interactive Editing**: Just **click** on the rendered label (e.g., `图1`) to reveal the source code (e.g., `@fig:id`) for editing. Move the cursor away to render it again.
+- **Smart Spacing**: Automatically hides spaces around citations (e.g., `... as shown in @fig:a ...` becomes `...如图1所示...`), perfect for Chinese typesetting.
+- **Attribute Support**: Correctly recognizes image attributes like `{#fig:id width=80%}`.
+
+## 📥 How to Install
+
+### Method 1: BRAT (Recommended)
+1. Install the **BRAT** plugin from the Obsidian Community Plugins.
+2. Add this repository URL: `https://github.com/wanxinhao/pandoc-live-preview`
+3. The plugin will be automatically installed and updated.
+
+### Method 2: Manual Installation
+1. Download `main.js`, `manifest.json`, and `styles.css` from the [Releases](../../releases) page.
+2. Create a folder named `pandoc-live-preview` in your vault's `.obsidian/plugins/` directory.
+3. Move the downloaded files into that folder.
+4. Reload Obsidian and enable the plugin.
+
+## ⚙️ Configuration (Settings)
+
+Go to **Settings** -> **Pandoc Live Preview** to configure:
+
+1. **PicGo Server URL**: Default is `http://127.0.0.1:36677/upload`.
+2. **Auto Upload from Clipboard**: Enable/Disable auto-upload when pasting images.
+3. **Delete Local File**:
+ * **On (Recommended)**: Uploads the image and deletes the local copy (keeps vault clean).
+ * **Off**: Uploads the image but keeps a local backup in your attachments folder.
+4. **Prefix Settings**: Customize the display prefix (e.g., change "图" to "Fig.", "表" to "Table").
+
+> ⚠️ **Conflict Warning**: If you are using **Image Auto Upload Plugin**, please **DISABLE** it to prevent conflicts. This plugin handles the upload logic natively.
+
+## 🚀 Usage
+
+**1. Define a Figure or Table:**
+* **Paste Image (New!)**: Just paste (`Ctrl+V`). The plugin uploads it and adds `{#fig:timestamp}` automatically.
+* **Manual**: Add a Pandoc ID `{#fig:name}` after your image.
+* **Automatic (Recommended)**: Use the command palette (`Ctrl/Cmd + P`) and search for **"Insert Figure ID"**. It will insert a unique ID based on the current time, like `{#fig:202512311844}`.
+ * *Tip: Bind this command to a hotkey (e.g., `Alt+F`) for maximum speed.*
+
+**2. Reference it:**
+Type **`@`** anywhere in your text. A menu will appear listing all defined figures and tables. Select one to insert the citation.
+> Result: `@fig:2025...` renders as **图1**
+
+## 🤝 Recommended
+
+To get the full academic writing preview experience (Citations + Cross-references), we highly recommend using this plugin alongside:
+
+* **[Pandoc Reference List](https://github.com/mgmeyers/obsidian-pandoc-reference-list)**
+ * It handles bibliography citations like `(Smith, 2021)` and displays a reference list in the sidebar.
+
+⚠️ This project will offer a better user experience when paired with [PaperBell] (https://github.com/PaperBell-Org/Obsidian-PaperBell).
+
+---
+
+
+# 中文说明 (Chinese Readme)
+
+这是一个专为 Obsidian 学术写作设计的插件,主要用于解决 Pandoc 交叉引用(Cross-ref)在 Obsidian 实时预览模式下无法直观显示的问题。
+
+**v1.5.0 重大更新**:现已集成 **PicGo 自动上传** 功能,打通了“截图 -> 粘贴 -> 上传 -> 引用”的全链路!
+
+## ✨ 核心功能
+
+- **实时渲染**:在编辑界面(Live Preview)直接将代码 `@fig:xxx` 渲染为 **图1**,将 `@tbl:xxx` 渲染为 **表1**。
+- 
+- **🖼️ 剪切板自动上传 (PicGo)**:
+ - 粘贴图片时,自动调用 PicGo 接口上传到图床。
+ - 自动生成带时间戳 ID 的标准引用:`{#fig:2025...}`。
+ - **自动清理**:上传成功后可选择自动删除本地的临时文件,保持仓库整洁。
+- **⚡ 智能补全**:输入 **`@`** 自动弹出文档内所有图表 ID 的建议菜单,告别死记硬背 ID 的痛苦。
+- 
+- **⚡ 一键生成 ID**:提供快捷命令插入基于“年月日时分”的唯一 ID(如 `{#fig:202512311800}`)。
+- 
+- 快捷键设置建议:
+- **点击即改**:鼠标点击渲染后的“图1”标签,或将光标移入,它会瞬间变回 `@fig:xxx` 源代码模式。
+- **属性支持**:完美支持带属性的写法,如 `{#fig:id width=14cm}`。
+- **无缝排版**:自动隐藏 Pandoc 语法建议保留的空格,让中文引用在视觉上连贯流畅。
+
+## 📥 安装方法
+
+### 方法 1:使用 BRAT 插件(推荐)
+1. 在 Obsidian 社区插件市场搜索并安装 **BRAT**。
+2. 在 BRAT 设置中点击 "Add Beta plugin"。
+3. 输入本仓库地址:`https://github.com/wanxinhao/pandoc-live-preview`。
+4. 点击添加,插件即可自动安装。
+
+### 方法 2:手动安装
+1.前往右侧的 [Releases](../../releases) 页面下载最新版本的附件(包含 `main.js`, `manifest.json`, `styles.css`)。
+2. 在你的 Obsidian 库的 `.obsidian/plugins/` 目录下新建文件夹 `pandoc-live-preview`。
+3. 将下载的三个文件放入该文件夹。
+4. 重启 Obsidian 并启用插件。
+
+## ⚙️ 插件设置 (Settings)
+
+请在 Obsidian 设置面板中找到 **Pandoc Live Preview** 进行配置:
+
+1. **PicGo 上传接口**:默认为 `http://127.0.0.1:36677/upload` (请确保 PicGo Server 已开启)。
+2. **剪切板自动上传**:开关粘贴图片时的自动上传功能。
+3. **上传后移除本地文件**:
+ * **开启 (推荐)**:上传图床成功后,自动删除本地附件文件夹中的临时文件,不占硬盘空间。
+ * **关闭**:上传的同时,在本地保留一份副本作为备份。
+4. **前缀设置**:在设置里即可修改图片和表格的前缀(例如改为 Fig. / Table),无需修改代码。
+
+> ⚠️ **冲突警告**:如果您正在使用 **Image Auto Upload Plugin**,请务必**关闭或禁用**它,否则会产生冲突。本插件已内置完整的上传逻辑。
+
+## 🚀 使用方法
+
+**1. 定义图表 ID**
+* **粘贴图片 (推荐)**:直接粘贴截图 (`Ctrl+V`),插件会自动上传并生成 `{#fig:时间戳}`。
+* **快捷生成**:打开命令面板 (`Ctrl/Cmd + P`),搜索 **"插入图片ID" (Insert Figure ID)**。插件会自动生成一个基于当前时间的唯一 ID。
+ * *建议:在设置里将此命令绑定快捷键(如 `Alt+F`),效率起飞。*
+* **手动输入**:在图片或表格后输入 `{#fig:name}`。
+
+**2. 引用图表**
+在正文中输入 **`@`** 符号,插件会自动弹出候选菜单,列出当前文档里所有的图和表。选中即可插入。
+> 效果:输入 `@fig:xxx` 后,光标移开即显示为 **图1**。
+
+## 🤝 推荐搭配
+
+为了获得完整的学术写作预览体验(参考文献 + 交叉引用),强烈推荐配合以下插件使用:
+
+* **[Pandoc Reference List](https://github.com/mgmeyers/obsidian-pandoc-reference-list)**
+ * 它可以预览 `(Smith, 2021)` 格式的参考文献,并在侧边栏显示文献列表。配合本插件,图表和文献都能实时预览。
+
+*⚠️ 该项目若与 [PaperBell](https://github.com/PaperBell-Org/Obsidian-PaperBell) 结合使用,会有更好的使用体验。
+* PaperBell: Research, to be connected
+* 👋 PaperBell 是使用 Obsidian 管理你学术生涯的终极方案。
+
+
+## 📄 License
+
+MIT License
diff --git a/PaperBell/.obsidian/plugins/pandoc-live-preview/data.json b/PaperBell/.obsidian/plugins/pandoc-live-preview/data.json
new file mode 100644
index 00000000..f34886fa
--- /dev/null
+++ b/PaperBell/.obsidian/plugins/pandoc-live-preview/data.json
@@ -0,0 +1,16 @@
+{
+ "picgoUrl": "http://127.0.0.1:36677/upload",
+ "autoUpload": true,
+ "addNewLineAroundImage": true,
+ "hideGapAroundImage": true,
+ "deleteLocal": true,
+ "figPrefix": "图",
+ "tblPrefix": "表",
+ "autoParentheses": true,
+ "enableClickToJump": true,
+ "captionColor": "#1e88e5",
+ "captionBold": true,
+ "captionCenter": true,
+ "referenceColor": "#1e88e5",
+ "referenceBold": true
+}
\ No newline at end of file
diff --git a/PaperBell/.obsidian/plugins/pandoc-live-preview/main.js b/PaperBell/.obsidian/plugins/pandoc-live-preview/main.js
new file mode 100644
index 00000000..94e86e68
--- /dev/null
+++ b/PaperBell/.obsidian/plugins/pandoc-live-preview/main.js
@@ -0,0 +1,544 @@
+/* main.js - Pandoc Live Preview Plugin (v2.0.1: Increase Suggest Limit) */
+const { Plugin, EditorSuggest, requestUrl, Notice, PluginSettingTab, Setting, FileSystemAdapter, ItemView, WorkspaceLeaf, Debounce } = require('obsidian');
+const { StateField } = require('@codemirror/state');
+const { Decoration, EditorView, WidgetType } = require('@codemirror/view');
+const path = require('path');
+
+// === 视图常量 ===
+const VIEW_TYPE_PANDOC_OUTLINE = "pandoc-outline-view";
+
+// === 默认设置 ===
+const DEFAULT_SETTINGS = {
+ picgoUrl: "http://127.0.0.1:36677/upload",
+ autoUpload: true,
+ addNewLineAroundImage: true,
+ hideGapAroundImage: true,
+ deleteLocal: true,
+ figPrefix: "图",
+ tblPrefix: "表",
+ autoParentheses: true,
+ enableClickToJump: true,
+
+ // 样式设置
+ captionColor: "#1e88e5",
+ captionBold: true,
+ captionCenter: true,
+ captionTopOffset: 6,
+ captionBottomDistance: 12,
+ referenceColor: "#1e88e5",
+ referenceBold: false
+};
+
+// === 辅助函数 ===
+function getTimestamp() {
+ const now = new Date();
+ return `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}${now.getHours().toString().padStart(2,'0')}${now.getMinutes().toString().padStart(2,'0')}${now.getSeconds().toString().padStart(2,'0')}`;
+}
+
+// === 1. 标签组件 (LabelWidget) ===
+class LabelWidget extends WidgetType {
+ constructor(text, type, isDef, caption = "", suffix = "", hasParen = false, targetPos = null, settings = null, status = "normal") {
+ super();
+ this.text = text;
+ this.type = type;
+ this.isDef = isDef;
+ this.caption = caption;
+ this.suffix = suffix;
+ this.hasParen = hasParen;
+ this.targetPos = targetPos;
+ this.settings = settings;
+ this.status = status;
+ }
+
+ toDOM(view) {
+ const span = document.createElement("span");
+ let content = this.text;
+
+ if (this.isDef) {
+ if (this.caption) content = `${this.text} ${this.caption}`;
+ } else {
+ if (this.status === "broken") {
+ content = `⛔ @${this.type}:${this.text}`;
+ } else {
+ content = `${this.text}${this.suffix}`;
+ if (this.hasParen) content = `(${content})`;
+ }
+ }
+
+ span.innerText = content;
+ span.className = `pandoc-widget pandoc-${this.type} pandoc-${this.isDef ? 'def' : 'ref'}`;
+
+ if (this.isDef && this.status === "unused") {
+ span.classList.add('pandoc-unused');
+ span.title = "警告:此图表未被引用";
+ }
+ if (!this.isDef && this.status === "broken") {
+ span.classList.add('pandoc-broken');
+ span.title = "错误:引用的ID不存在!";
+ }
+
+ if (this.settings && this.status !== "broken") {
+ if (this.isDef) {
+ if (this.status !== "unused" && this.settings.captionColor) span.style.color = this.settings.captionColor;
+ span.style.fontWeight = this.settings.captionBold ? '600' : '400';
+ span.style.textAlign = this.settings.captionCenter ? 'center' : 'left';
+ span.style.marginTop = `${this.settings.captionTopOffset}px`;
+ span.style.marginBottom = `${this.settings.captionBottomDistance}px`;
+ } else {
+ if (this.settings.referenceColor) span.style.color = this.settings.referenceColor;
+ span.style.fontWeight = this.settings.referenceBold ? '600' : '400';
+ }
+ }
+
+ if(this.caption) span.classList.add('has-caption');
+
+ if (!this.isDef && this.settings && this.settings.enableClickToJump && this.targetPos !== null && this.status !== "broken") {
+ span.classList.add('pandoc-clickable');
+ span.onclick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ view.dispatch({ selection: { anchor: this.targetPos, head: this.targetPos }, scrollIntoView: true });
+ };
+ }
+ return span;
+ }
+}
+
+// === 2. 隐藏组件 ===
+class TopGapWidget extends WidgetType {
+ toDOM(view) { const span = document.createElement("span"); span.className = "pandoc-gap"; return span; }
+}
+
+// === 3. 全文审计扫描 (Audit) ===
+let currentSettings = Object.assign({}, DEFAULT_SETTINGS);
+
+function scanDocumentStats(text, settings) {
+ const definitions = [];
+ const definedIds = new Set();
+ const references = [];
+ const undefinedImages = [];
+
+ const FIG_PRE = settings.figPrefix;
+ const TBL_PRE = settings.tblPrefix;
+
+ // 1. 扫描定义 ({#fig:xxx})
+ const defRegex = /\{#(fig|tbl):([a-zA-Z0-9_\-]+)(?:\s+.*?)?\}/g;
+ let defMatch;
+ let figCount = 0;
+ let tblCount = 0;
+
+ while ((defMatch = defRegex.exec(text)) !== null) {
+ const type = defMatch[1];
+ const id = defMatch[2];
+ let label = "";
+ if (type === 'fig') label = `${FIG_PRE}${++figCount}`;
+ else if (type === 'tbl') label = `${TBL_PRE}${++tblCount}`;
+
+ // 提取标题
+ let caption = id;
+ const lookBackText = text.slice(Math.max(0, defMatch.index - 500), defMatch.index);
+ // 修复:提取标题时也支持尖括号路径
+ const imgMatch = lookBackText.match(/!\[([^\]]*)\]\((?:<[^>]+>|[^\)]+)\)\s*$/);
+ if (imgMatch && imgMatch[1]) caption = imgMatch[1].trim();
+ const tblMatch = lookBackText.match(/(:[^\r\n{]+)$/);
+ if (tblMatch) caption = tblMatch[1].substring(1).trim();
+
+ definitions.push({
+ id: id, type: type, label: label, fullId: `${type}:${id}`,
+ position: defMatch.index, caption: caption || id,
+ isUnused: true
+ });
+ definedIds.add(id);
+ }
+
+ // 2. 扫描引用 (@fig:xxx)
+ const refRegex = /@(fig|tbl):([a-zA-Z0-9_\-]+)/g;
+ let refMatch;
+ while ((refMatch = refRegex.exec(text)) !== null) {
+ const id = refMatch[2];
+ const isBroken = !definedIds.has(id);
+
+ references.push({
+ id: id, type: refMatch[1], position: refMatch.index,
+ fullText: refMatch[0], isBroken: isBroken
+ });
+ }
+
+ // 3. 修正定义的未使用状态
+ const usedIds = new Set(references.map(r => r.id));
+ definitions.forEach(def => {
+ if (usedIds.has(def.id)) def.isUnused = false;
+ });
+
+ // 4. 扫描未定义ID的图片 (【核心修复】:支持尖括号路径 和普通路径)
+ // 解释:\((?:<[^>]+>|[^\)]+)\)
+ // (?: ... ) 是非捕获组
+ // <[^>]+> 匹配尖括号包裹的内容 (处理含括号的路径)
+ // | 或者
+ // [^\)]+ 匹配不含右括号的普通路径
+ const imgRegex = /!\[([^\]]*)\]\((?:<[^>]+>|[^\)]+)\)/g;
+ let imgM;
+ while ((imgM = imgRegex.exec(text)) !== null) {
+ const endPos = imgM.index + imgM[0].length;
+ const nextText = text.slice(endPos, endPos + 50);
+ if (!/^\s*\{#fig:/.test(nextText)) {
+ undefinedImages.push({ caption: imgM[1] || "未命名图片", position: imgM.index });
+ }
+ }
+
+ // 5. 提取失效引用
+ const orphanRefs = references.filter(r => r.isBroken);
+
+ return { definitions, references, orphanRefs, undefinedImages };
+}
+
+// === 4. 核心装饰器 ===
+const pandocRefField = StateField.define({
+ create(state) { return Decoration.none; },
+ update(oldDecorations, transaction) {
+ if (!transaction.docChanged && !transaction.selection) return oldDecorations;
+
+ const state = transaction.state;
+ const text = state.doc.toString();
+ const widgets = [];
+ const selectionRanges = state.selection.ranges;
+
+ const { definitions, definedIds } = scanDocumentStats(text, currentSettings);
+
+ const figMap = new Map();
+ const tblMap = new Map();
+ const posMap = new Map();
+ const unusedMap = new Map();
+ const defSet = new Set();
+
+ definitions.forEach(def => {
+ if (def.type === 'fig') figMap.set(def.id, def.label.replace(currentSettings.figPrefix, ''));
+ if (def.type === 'tbl') tblMap.set(def.id, def.label.replace(currentSettings.tblPrefix, ''));
+ posMap.set(def.id, def.position);
+ unusedMap.set(def.id, def.isUnused);
+ defSet.add(def.id);
+ });
+
+ function checkCursorOverlap(start, end) {
+ for (const range of selectionRanges) { if (range.from <= end && range.to >= start) return true; }
+ return false;
+ }
+ function consumeImmediateNewlines(text, currentStart) {
+ let tempStart = currentStart;
+ while (tempStart > 0) {
+ if (text[tempStart - 1].match(/[\n\r ]/)) tempStart--; else break;
+ }
+ return tempStart;
+ }
+ function consumeHorizontalSpaces(text, pos) {
+ while (pos < text.length) {
+ if (text[pos] === ' ' || text[pos] === '\t') pos++; else break;
+ }
+ return pos;
+ }
+
+ function addDecoration(start, end, type, id, isDef, suffix = "", hasParen = false) {
+ let number = "?";
+ let prefix = type === 'fig' ? currentSettings.figPrefix : currentSettings.tblPrefix;
+ let caption = "";
+ let targetPos = null;
+ let status = "normal";
+
+ if (isDef) {
+ if (unusedMap.has(id) && unusedMap.get(id)) status = "unused";
+ } else {
+ if (!defSet.has(id)) status = "broken";
+ }
+
+ if (type === 'fig') {
+ if (figMap.has(id)) number = figMap.get(id);
+ if (isDef) {
+ const lookBackLimit = Math.max(0, start - 500);
+ const precedingText = text.slice(lookBackLimit, start);
+ const imgMatch = precedingText.match(/!\[([^\]]*)\]\([^\)]+\)\s*$/);
+
+ if (imgMatch) {
+ caption = imgMatch[1].trim();
+ if (currentSettings.hideGapAroundImage) {
+ const imgStartPos = start - imgMatch[0].length;
+ if (imgStartPos > 0 && text[imgStartPos - 1] === '\n') {
+ const gapStart = imgStartPos - 1;
+ if (!checkCursorOverlap(gapStart, imgStartPos)) widgets.push(Decoration.replace({ widget: new TopGapWidget(), inclusive: false }).range(gapStart, imgStartPos));
+ }
+ start = consumeImmediateNewlines(text, start);
+ }
+ }
+ if (currentSettings.hideGapAroundImage) {
+ let checkPos = consumeHorizontalSpaces(text, end);
+ if (checkPos < text.length) {
+ if (text.startsWith('\r\n', checkPos)) end = checkPos + 2;
+ else if (text[checkPos] === '\n') end = checkPos + 1;
+ }
+ }
+ }
+ } else if (type === 'tbl') {
+ if (tblMap.has(id)) number = tblMap.get(id);
+ if (isDef) {
+ const lookBackLimit = Math.max(0, start - 1000);
+ const match = text.slice(lookBackLimit, start).match(/(:[^\r\n{]+)$/);
+ if (match) {
+ caption = match[1].substring(1).trim();
+ start = start - match[0].length;
+ start = consumeImmediateNewlines(text, start);
+ }
+ }
+ }
+
+ if (!isDef && posMap.has(id)) targetPos = posMap.get(id);
+ if (checkCursorOverlap(start, end)) return;
+
+ const widgetText = (status === "broken") ? id : `${prefix}${number}`;
+
+ const deco = Decoration.replace({
+ widget: new LabelWidget(widgetText, type, isDef, caption, suffix, hasParen, targetPos, currentSettings, status),
+ inclusive: false
+ }).range(start, end);
+ widgets.push(deco);
+ }
+
+ const defRegex = /\{#(fig|tbl):([a-zA-Z0-9_\-]+)(?:\s+.*?)?\}/g;
+ let defMatch;
+ while ((defMatch = defRegex.exec(text)) !== null) addDecoration(defMatch.index, defMatch.index + defMatch[0].length, defMatch[1], defMatch[2], true);
+
+ const refRegex = /(\(?[ \t]*)@(fig|tbl):([a-zA-Z0-9_\-]+)(?:[ \t]+([a-zA-Z]))?([ \t]*\)?)/g;
+ let refMatch;
+ while ((refMatch = refRegex.exec(text)) !== null) {
+ const hasParen = (refMatch[1].includes('(') && refMatch[5].includes(')'));
+ addDecoration(refMatch.index, refMatch.index + refMatch[0].length, refMatch[2], refMatch[3], false, refMatch[4] || "", hasParen);
+ }
+
+ return Decoration.set(widgets.sort((a, b) => a.from - b.from));
+ },
+ provide: (field) => EditorView.decorations.from(field)
+});
+
+// === 5. 侧边栏视图 ===
+class PandocOutlineView extends ItemView {
+ constructor(leaf, plugin) {
+ super(leaf);
+ this.plugin = plugin;
+ this.updateDebounce = this.debounce(this.updateView.bind(this), 500);
+ }
+
+ getViewType() { return VIEW_TYPE_PANDOC_OUTLINE; }
+ getDisplayText() { return "图表审计 (Pandoc)"; }
+ getIcon() { return "image-file"; }
+
+ async onOpen() {
+ this.registerEvent(this.app.workspace.on('editor-change', this.updateDebounce));
+ this.registerEvent(this.app.workspace.on('file-open', this.updateView.bind(this)));
+ this.updateView();
+ }
+
+ async updateView() {
+ const container = this.contentEl;
+ container.empty();
+ container.addClass("pandoc-outline-container");
+
+ const view = this.app.workspace.getActiveViewOfType(require('obsidian').MarkdownView);
+ if (!view) {
+ container.createEl("div", { text: "未激活 Markdown 编辑器" });
+ return;
+ }
+
+ const text = view.editor.getValue();
+ const { definitions, orphanRefs, undefinedImages } = scanDocumentStats(text, this.plugin.settings);
+
+ const header = container.createEl("div", { cls: "pandoc-outline-header" });
+ header.createEl("div", { text: `📊 统计概览` });
+ header.createEl("small", { text: `定义: ${definitions.length} | 引用失效: ${orphanRefs.length} | 未定义: ${undefinedImages.length}`, style: "opacity:0.8" });
+
+ if (orphanRefs.length > 0) {
+ container.createEl("div", { cls: "pandoc-section-title", text: "⛔ 引用失效 (找不到定义)", style: "color: #d32f2f;" });
+ orphanRefs.forEach(ref => {
+ const el = container.createEl("div", { cls: "pandoc-outline-item pandoc-item-broken" });
+ el.createEl("span", { text: ref.fullText });
+ el.createEl("small", { text: `行 ${view.editor.offsetToPos(ref.position).line + 1}` });
+ el.addEventListener("click", () => {
+ view.editor.setCursor(view.editor.offsetToPos(ref.position));
+ view.editor.focus();
+ view.editor.scrollIntoView({ from: view.editor.offsetToPos(ref.position), to: view.editor.offsetToPos(ref.position) }, true);
+ });
+ });
+ }
+
+ if (undefinedImages.length > 0) {
+ container.createEl("div", { cls: "pandoc-section-title", text: "⚠️ 未打标签的图片", style: "color: #7f8c8d;" });
+ undefinedImages.forEach(img => {
+ const el = container.createEl("div", { cls: "pandoc-outline-item pandoc-item-missing-id" });
+ el.createEl("span", { text: img.caption });
+ el.addEventListener("click", () => {
+ view.editor.setCursor(view.editor.offsetToPos(img.position));
+ view.editor.focus();
+ view.editor.scrollIntoView({ from: view.editor.offsetToPos(img.position), to: view.editor.offsetToPos(img.position) }, true);
+ });
+ });
+ }
+
+ if (definitions.length > 0) {
+ container.createEl("div", { cls: "pandoc-section-title", text: "📑 图表定义列表" });
+ definitions.forEach(def => {
+ let itemCls = "pandoc-item-normal";
+ let statusText = "";
+ if (def.isUnused) {
+ itemCls = "pandoc-item-unused";
+ statusText = "(未使用)";
+ }
+ const el = container.createEl("div", { cls: `pandoc-outline-item ${itemCls}` });
+ el.createEl("span", { text: `${def.label} ${def.caption}` });
+ if(statusText) el.createEl("span", { text: statusText, cls: "pandoc-tag" });
+ el.addEventListener("click", () => {
+ view.editor.setCursor(view.editor.offsetToPos(def.position));
+ view.editor.focus();
+ view.editor.scrollIntoView({ from: view.editor.offsetToPos(def.position), to: view.editor.offsetToPos(def.position) }, true);
+ });
+ });
+ }
+ }
+
+ debounce(func, wait) {
+ let timeout;
+ return function(...args) {
+ const context = this;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func.apply(context, args), wait);
+ };
+ }
+}
+
+// === 6. 自动补全 ===
+class PandocSuggest extends EditorSuggest {
+ constructor(plugin) {
+ super(plugin.app);
+ this.plugin = plugin;
+ this.limit = 1000; // 【核心修复】增加建议数量限制,解决大文档显示不全问题
+ }
+ onTrigger(cursor, editor, file) {
+ const line = editor.getLine(cursor.line);
+ const sub = line.substring(0, cursor.ch);
+ const match = sub.match(/(@(fig|tbl)?:?([a-zA-Z0-9_\-]*))$/);
+ if (match) return { start: { line: cursor.line, ch: match.index }, end: cursor, query: match[0] };
+ return null;
+ }
+ getSuggestions(context) {
+ const text = context.editor.getValue();
+ const { definitions } = scanDocumentStats(text, this.plugin.settings);
+ const query = context.query.toLowerCase();
+ return definitions.filter(def => `@${def.type}:${def.id}`.toLowerCase().includes(query))
+ .map(def => ({ ...def, suggestionText: `@${def.type}:${def.id}` }));
+ }
+ renderSuggestion(suggestion, el) {
+ el.createEl("span", { text: suggestion.label, cls: "pandoc-suggest-label" });
+ el.createEl("small", { text: ` (${suggestion.id})`, cls: "pandoc-suggest-id" });
+ }
+ selectSuggestion(suggestion, event) {
+ const context = this.context;
+ if (!context) return;
+ let textToInsert = suggestion.suggestionText;
+ if (this.plugin.settings.autoParentheses) textToInsert = `( ${textToInsert} )`;
+ context.editor.replaceRange(textToInsert, context.start, context.end);
+ }
+}
+
+// === 7. 设置面板 ===
+class PandocLivePreviewSettingTab extends PluginSettingTab {
+ constructor(app, plugin) { super(app, plugin); this.plugin = plugin; }
+ display() {
+ const { containerEl } = this;
+ containerEl.empty();
+ containerEl.createEl('h2', { text: 'Pandoc Live Preview v2.0.1 设置' });
+
+ new Setting(containerEl).setName('PicGo 上传接口').addText(t => t.setValue(this.plugin.settings.picgoUrl).onChange(async v => { this.plugin.settings.picgoUrl = v; await this.plugin.saveSettings(); }));
+ new Setting(containerEl).setName('剪切板自动上传').addToggle(t => t.setValue(this.plugin.settings.autoUpload).onChange(async v => { this.plugin.settings.autoUpload = v; await this.plugin.saveSettings(); }));
+ new Setting(containerEl).setName('图片前后增加空行').addToggle(t => t.setValue(this.plugin.settings.addNewLineAroundImage).onChange(async v => { this.plugin.settings.addNewLineAroundImage = v; await this.plugin.saveSettings(); }));
+ new Setting(containerEl).setName('预览时隐藏图片空行').addToggle(t => t.setValue(this.plugin.settings.hideGapAroundImage).onChange(async v => { this.plugin.settings.hideGapAroundImage = v; this.plugin.app.workspace.updateOptions(); await this.plugin.saveSettings(); }));
+ new Setting(containerEl).setName('上传后移除本地文件').addToggle(t => t.setValue(this.plugin.settings.deleteLocal).onChange(async v => { this.plugin.settings.deleteLocal = v; await this.plugin.saveSettings(); }));
+
+ containerEl.createEl('h3', { text: '视觉与位置' });
+ new Setting(containerEl).setName('图表名颜色').addColorPicker(c => c.setValue(this.plugin.settings.captionColor).onChange(async v => { this.plugin.settings.captionColor = v; currentSettings.captionColor = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+ new Setting(containerEl).setName('图表名加粗').addToggle(t => t.setValue(this.plugin.settings.captionBold).onChange(async v => { this.plugin.settings.captionBold = v; currentSettings.captionBold = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+ new Setting(containerEl).setName('图表名居中显示').addToggle(t => t.setValue(this.plugin.settings.captionCenter).onChange(async v => { this.plugin.settings.captionCenter = v; currentSettings.captionCenter = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+
+ new Setting(containerEl).setName('图表名 上方 间距').addSlider(s => s.setLimits(-50, 50, 1).setValue(this.plugin.settings.captionTopOffset).setDynamicTooltip().onChange(async v => { this.plugin.settings.captionTopOffset = v; currentSettings.captionTopOffset = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+ new Setting(containerEl).setName('图表名 下方 间距').addSlider(s => s.setLimits(0, 100, 1).setValue(this.plugin.settings.captionBottomDistance).setDynamicTooltip().onChange(async v => { this.plugin.settings.captionBottomDistance = v; currentSettings.captionBottomDistance = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+
+ new Setting(containerEl).setName('引用处颜色').addColorPicker(c => c.setValue(this.plugin.settings.referenceColor).onChange(async v => { this.plugin.settings.referenceColor = v; currentSettings.referenceColor = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+ new Setting(containerEl).setName('引用处加粗').addToggle(t => t.setValue(this.plugin.settings.referenceBold).onChange(async v => { this.plugin.settings.referenceBold = v; currentSettings.referenceBold = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+
+ new Setting(containerEl).setName('启用单击跳转').addToggle(t => t.setValue(this.plugin.settings.enableClickToJump).onChange(async v => { this.plugin.settings.enableClickToJump = v; this.plugin.app.workspace.updateOptions(); await this.plugin.saveSettings(); }));
+ new Setting(containerEl).setName('引用自动加括号').addToggle(t => t.setValue(this.plugin.settings.autoParentheses).onChange(async v => { this.plugin.settings.autoParentheses = v; await this.plugin.saveSettings(); }));
+
+ containerEl.createEl('h3', { text: '前缀设置' });
+ new Setting(containerEl).setName('图片前缀').addText(t => t.setValue(this.plugin.settings.figPrefix).onChange(async v => { this.plugin.settings.figPrefix = v; currentSettings.figPrefix = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+ new Setting(containerEl).setName('表格前缀').addText(t => t.setValue(this.plugin.settings.tblPrefix).onChange(async v => { this.plugin.settings.tblPrefix = v; currentSettings.tblPrefix = v; await this.plugin.saveSettings(); this.plugin.app.workspace.updateOptions(); }));
+ }
+}
+
+// === 8. 插件主类 ===
+module.exports = class PandocLivePreview extends Plugin {
+ async onload() {
+ await this.loadSettings();
+ currentSettings = this.settings;
+ this.registerView(VIEW_TYPE_PANDOC_OUTLINE, (leaf) => new PandocOutlineView(leaf, this));
+ this.registerEditorExtension(pandocRefField);
+ this.registerEditorSuggest(new PandocSuggest(this));
+ this.addSettingTab(new PandocLivePreviewSettingTab(this.app, this));
+ this.addCommand({ id: 'insert-fig-id', name: 'Insert Figure ID', editorCallback: (e) => e.replaceSelection(`{#fig:${getTimestamp()}}`) });
+ this.addCommand({ id: 'insert-tbl-id', name: 'Insert Table ID', editorCallback: (e) => e.replaceSelection(`{#tbl:${getTimestamp()}}`) });
+ this.addCommand({ id: 'open-pandoc-outline', name: '打开图表管理面板 (Pandoc Manager)', callback: () => this.activateView() });
+ this.addRibbonIcon('image-file', 'Pandoc 图表管理', () => { this.activateView(); });
+ this.registerEvent(this.app.workspace.on('editor-paste', this.handleImagePaste.bind(this)));
+ }
+
+ async activateView() {
+ const { workspace } = this.app;
+ let leaf = workspace.getLeavesOfType(VIEW_TYPE_PANDOC_OUTLINE)[0];
+ if (!leaf) {
+ const rightLeaf = workspace.getRightLeaf(false);
+ if (rightLeaf) { await rightLeaf.setViewState({ type: VIEW_TYPE_PANDOC_OUTLINE, active: true }); leaf = workspace.getLeavesOfType(VIEW_TYPE_PANDOC_OUTLINE)[0]; }
+ }
+ if (leaf) workspace.revealLeaf(leaf);
+ }
+
+ async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); }
+ async saveSettings() { await this.saveData(this.settings); }
+
+ async handleImagePaste(evt, editor, view) {
+ if (!this.settings.autoUpload) return;
+ if (evt.clipboardData.files.length > 0) {
+ const file = evt.clipboardData.files[0];
+ if (file.type.startsWith('image/')) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ const timestamp = getTimestamp();
+ const placeholder = ``;
+ editor.replaceSelection(placeholder);
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const extension = file.type.split('/')[1] || 'png';
+ const fileName = `Image_${timestamp}.${extension}`;
+ const filePath = await this.app.fileManager.getAvailablePathForAttachment(fileName);
+ await this.app.vault.createBinary(filePath, arrayBuffer);
+ let absolutePath;
+ if (this.app.vault.adapter instanceof FileSystemAdapter) absolutePath = path.join(this.app.vault.adapter.getBasePath(), filePath);
+ else { new Notice("PicGo 上传仅支持桌面版"); return; }
+ const response = await requestUrl({ url: this.settings.picgoUrl, method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ list: [absolutePath] }) });
+ const resData = response.json;
+ if (resData.success && resData.result && resData.result.length > 0) {
+ const remoteUrl = resData.result[0];
+ let finalStr = `{#fig:${timestamp}}`;
+ if (this.settings.addNewLineAroundImage) finalStr = `\n\n${finalStr}\n\n`;
+ const doc = editor.getValue();
+ if (doc.includes(placeholder)) { editor.setValue(doc.replace(placeholder, finalStr)); new Notice(`图片上传成功!`); } else { editor.replaceSelection(finalStr); }
+ if (this.settings.deleteLocal) { const fileToDelete = this.app.vault.getAbstractFileByPath(filePath); if (fileToDelete) await this.app.vault.delete(fileToDelete); }
+ } else { new Notice("PicGo 上传失败"); editor.setValue(editor.getValue().replace(placeholder, "")); }
+ } catch (error) { console.error("Upload Error:", error); new Notice(`上传出错: ${error.message}`); editor.setValue(editor.getValue().replace(placeholder, "")); }
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/PaperBell/.obsidian/plugins/pandoc-live-preview/manifest.json b/PaperBell/.obsidian/plugins/pandoc-live-preview/manifest.json
new file mode 100644
index 00000000..c3854744
--- /dev/null
+++ b/PaperBell/.obsidian/plugins/pandoc-live-preview/manifest.json
@@ -0,0 +1,10 @@
+{
+ "id": "pandoc-live-preview",
+ "name": "Pandoc Live Preview",
+ "version": "3.5.0",
+ "minAppVersion": "0.15.0",
+ "description": "Real-time preview for Pandoc citations (@fig:xxx -> 图 1).",
+ "author": "EdgerHao",
+ "authorUrl": "",
+ "isDesktopOnly": false
+}
\ No newline at end of file
diff --git a/PaperBell/.obsidian/plugins/pandoc-live-preview/styles.css b/PaperBell/.obsidian/plugins/pandoc-live-preview/styles.css
new file mode 100644
index 00000000..72f38af5
--- /dev/null
+++ b/PaperBell/.obsidian/plugins/pandoc-live-preview/styles.css
@@ -0,0 +1,115 @@
+/* styles.css - v3.5.0 (Full Audit) */
+
+/* === 1. 基础组件 === */
+.pandoc-widget {
+ border-radius: 4px;
+ padding: 0;
+ font-size: 0.9em;
+ transition: all 0.2s ease;
+ user-select: none;
+}
+
+/* === 2. 引用处 (Reference) === */
+.pandoc-ref {
+ background-color: rgba(var(--color-accent-rgb), 0.12);
+ display: inline;
+ padding: 0 2px;
+}
+.pandoc-ref:hover {
+ background-color: rgba(var(--color-accent-rgb), 0.3);
+ text-decoration: underline;
+}
+.pandoc-ref.pandoc-clickable { cursor: pointer; }
+.pandoc-ref:not(.pandoc-clickable) { cursor: text; }
+
+/* 【新增】失效引用 (Broken Reference) */
+/* 当 @fig:xxx 找不到对应的 {#fig:xxx} 时 */
+.pandoc-ref.pandoc-broken {
+ color: #ffffff !important;
+ background-color: #d32f2f !important; /* 红底白字,醒目 */
+ font-weight: bold;
+ border-radius: 4px;
+ padding: 0 4px;
+ text-decoration: line-through; /* 删除线 */
+}
+
+/* === 3. 定义处 (Definition) === */
+.pandoc-def {
+ display: block;
+ width: 100%;
+ line-height: 1.4;
+ font-size: 0.9em;
+ background-color: transparent;
+ border: none;
+ cursor: text;
+ margin: 0;
+}
+
+/* 定义了但未被引用 -> 标橙警告 */
+.pandoc-def.pandoc-unused {
+ color: #e67e22 !important;
+ text-decoration: underline wavy #e67e22;
+}
+
+/* === 4. 图片标题 === */
+.pandoc-fig.pandoc-def {
+ color: #1e88e5;
+ margin-top: 6px !important;
+ margin-bottom: 12px !important;
+}
+
+/* === 5. 表格标题 === */
+.pandoc-tbl.pandoc-def {
+ margin-top: 6px !important;
+ margin-bottom: 6px !important;
+}
+
+/* === 6. 侧边栏大纲 (Pandoc Outline View) === */
+.pandoc-outline-container {
+ padding: 10px;
+}
+.pandoc-outline-header {
+ font-weight: bold;
+ margin-bottom: 10px;
+ border-bottom: 1px solid var(--background-modifier-border);
+ padding-bottom: 5px;
+}
+.pandoc-section-title {
+ margin-top: 15px;
+ margin-bottom: 5px;
+ font-weight: bold;
+ font-size: 0.9em;
+ display: flex;
+ justify-content: space-between;
+}
+
+.pandoc-outline-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-bottom: 2px;
+ font-size: 0.85em;
+}
+.pandoc-outline-item:hover {
+ background-color: var(--background-modifier-hover);
+}
+
+/* 侧边栏状态颜色 */
+.pandoc-item-normal { color: var(--text-normal); }
+.pandoc-item-unused { color: #e67e22; } /* 橙色:未使用 */
+.pandoc-item-broken { color: #d32f2f; font-weight: bold; } /* 红色:引用失效 */
+.pandoc-item-missing-id { color: #7f8c8d; font-style: italic; } /* 灰色:未定义ID */
+
+.pandoc-tag {
+ font-size: 0.75em;
+ opacity: 0.7;
+ margin-left: 8px;
+}
+
+/* === 7. 辅助 === */
+.pandoc-gap { display: none; }
+.pandoc-suggest-label { font-weight: bold; color: var(--text-accent); margin-right: 8px; }
+.pandoc-suggest-id { color: var(--text-muted); font-size: 0.85em; }
\ No newline at end of file
diff --git a/PaperBell/.obsidian/plugins/paperbell/data.json b/PaperBell/.obsidian/plugins/paperbell/data.json
index 05daac69..516977d3 100644
--- a/PaperBell/.obsidian/plugins/paperbell/data.json
+++ b/PaperBell/.obsidian/plugins/paperbell/data.json
@@ -1,11 +1,11 @@
{
- "registrationId": "",
+ "registrationId": "PB-6B438B-F8AD-AF2A",
"noteLocation": "Locations/Institutes",
"institutionNoteTemplate": "40 - Obsidian/模板/机构模板pro",
"autoGenerateInstitutionNotes": true,
"openInstitutionAfterCreation": true,
"listenToFileCreatedInPath": "Persons/Scholars",
- "enableUpdater": false,
+ "enableUpdater": true,
"landing": {
"firstLoad": false,
"completedSteps": {
@@ -438,4 +438,4 @@
}
}
}
-}
+}
\ No newline at end of file
From 259d1631487f739c71f5f9c518f437e5d8ce3d27 Mon Sep 17 00:00:00 2001
From: EdgerHao <112294576+EdgerHao@users.noreply.github.com>
Date: Sun, 12 Apr 2026 19:58:50 +0800
Subject: [PATCH 2/2] Update repository URL in README for BRAT plugin
---
PaperBell/.obsidian/plugins/pandoc-live-preview/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md b/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md
index 95d6d211..241fd446 100644
--- a/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md
+++ b/PaperBell/.obsidian/plugins/pandoc-live-preview/README.md
@@ -97,7 +97,7 @@ To get the full academic writing preview experience (Citations + Cross-reference
### 方法 1:使用 BRAT 插件(推荐)
1. 在 Obsidian 社区插件市场搜索并安装 **BRAT**。
2. 在 BRAT 设置中点击 "Add Beta plugin"。
-3. 输入本仓库地址:`https://github.com/wanxinhao/pandoc-live-preview`。
+3. 输入本仓库地址:`https://github.com/EdgerHao/pandoc-live-preview`。
4. 点击添加,插件即可自动安装。
### 方法 2:手动安装