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**。 +- ![](https://wanxinhao88.oss-cn-wuhan-lr.aliyuncs.com/img/20251231135057667.png) +- **🖼️ 剪切板自动上传 (PicGo)**: + - 粘贴图片时,自动调用 PicGo 接口上传到图床。 + - 自动生成带时间戳 ID 的标准引用:`![](URL){#fig:2025...}`。 + - **自动清理**:上传成功后可选择自动删除本地的临时文件,保持仓库整洁。 +- **⚡ 智能补全**:输入 **`@`** 自动弹出文档内所有图表 ID 的建议菜单,告别死记硬背 ID 的痛苦。 +- ![](https://wanxinhao88.oss-cn-wuhan-lr.aliyuncs.com/img/20251231190922619.png) +- **⚡ 一键生成 ID**:提供快捷命令插入基于“年月日时分”的唯一 ID(如 `{#fig:202512311800}`)。 +- ![](https://wanxinhao88.oss-cn-wuhan-lr.aliyuncs.com/img/20251231191509696.png) +- 快捷键设置建议:![](https://wanxinhao88.oss-cn-wuhan-lr.aliyuncs.com/img/20251231195516689.png) +- **点击即改**:鼠标点击渲染后的“图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 = `![Uploading...](${timestamp})`; + 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 = `![](${remoteUrl}){#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:手动安装