From 0e051b7293ec8578d154cc207c22433ef4cd4c1c Mon Sep 17 00:00:00 2001 From: nan Date: Wed, 22 Apr 2026 11:08:20 +0800 Subject: [PATCH] feat: Rename plugin to Odox and enhance documentation for static site export features --- README.md | 85 +++++++++++------ manifest.json | 6 +- notes/implementation.md | 103 ++++++++++++-------- package.json | 4 +- src/main.ts | 202 +++++++++++++++++++++++----------------- src/settings.ts | 47 +++++++--- styles.css | 22 +++++ 7 files changed, 298 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 93e7c9b..87d0bf7 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,96 @@ -# VitePress Exporter - Obsidian Plugin +# Odox - Obsidian Plugin ## 功能 -这个 Obsidian 插件用于自动化发布 Obsidian 笔记到 VitePress 网站: +这个 Obsidian 插件用于自动化发布 Obsidian 笔记到静态文档网站(如 VitePress、Docusaurus 等): - ✅ 将当前 markdown 文件导出到 `docs/` 目录 -- ✅ 自动复制引用的资源文件到 `docs/assets/` 目录 -- ✅ 更新 markdown 中的资源路径 -- 🚧 Canvas 文件渲染(待实现) +- ✅ 自动复制引用的资源文件到 `docs/public/` 目录 +- ✅ 更新 markdown 中的资源路径为根路径引用 +- ✅ Canvas 文件渲染为高清 PNG 图片 +- ✅ 支持 Canvas 透明背景导出 +- ✅ 灵活的目录结构和命名规则配置 ## 安装 -插件已经安装在这个项目的 `.obsidian/plugins/vitepress-exporter/` 目录下。 +插件已经安装在这个项目的 `.obsidian/plugins/odox/` 目录下。 1. 在 Obsidian 中打开这个项目 2. 进入设置 → 第三方插件 -3. 启用 "VitePress Exporter" 插件 +3. 启用 "Odox" 插件 ## 使用 ### 方式 1:命令面板 -1. 打开一个 markdown 文件 +1. 打开一个 markdown 或 canvas 文件 2. 按 `Ctrl+P` (或 `Cmd+P`) 打开命令面板 -3. 输入 "Export to VitePress" -4. 选择 "Export current note to VitePress" +3. 输入 "Export to docs" +4. 选择 "Export current note to docs" -### 方式 2:功能区按钮 -- 点击左侧功能区栏的上传图标按钮 +### 方式 2:文件菜单 +- 在文件管理器中右键点击文件 +- 选择 "Export to docs" 选项 -### 方式 3:快捷键 +### 方式 3:编辑器菜单 +- 在编辑器中右键点击 +- 选择 "Export to docs" 选项 + +### 方式 4:快捷键 - 可以在 Obsidian 的快捷键设置中为导出命令设置快捷键 ## 配置 在插件设置中可以配置: -- **Docs path**: 导出的 markdown 文件存放目录(默认:`docs`) -- **Assets path**: 资源文件存放目录(默认:`docs/assets`) +- **Wiki path**: 导出的 markdown 文件存放目录(默认:`docs`) +- **Assets path**: 资源文件存放目录(默认:`docs/public`) +- **Nest note folder**: 是否为每个笔记创建文件夹(默认:开启) +- **Preserve folder structure**: 是否保留原始文件夹结构(默认:开启) +- **Transparent canvas background**: 导出 Canvas 时使用透明背景(默认:开启) +- **Canvas embed template**: Canvas 嵌入模板,使用 `{url}` 作为图片路径占位符 ## 工作流程 1. 用户在 Obsidian 中编写笔记 -2. 笔记中引用的资源(图片、PDF 等)可以放在任意位置 -3. 点击导出按钮 +2. 笔记中引用的资源(图片、Canvas 等)可以放在任意位置 +3. 触发导出命令 4. 插件自动: - 将 markdown 文件复制到 `docs/` 目录 - - 扫描并复制所有引用的资源到 `docs/assets/` 目录 - - 更新 markdown 中的资源路径 + - 扫描并复制所有引用的资源到 `docs/public/` 目录 + - 将 Canvas 文件渲染为高清 PNG 图片 + - 更新 markdown 中的资源路径为根路径引用(如 `/image.png`) + +## Canvas 导出 + +Canvas 文件会通过以下方式处理: + +1. 使用 Obsidian 内部 API 渲染 Canvas 为 PNG 图片 +2. 保存图片到 `docs/public/` 目录 +3. 生成对应的 `.canvas.md` 文件,使用配置的嵌入模板 +4. 支持透明背景导出(移除 Obsidian 的点状背景) ## 当前状态 - ✅ 基础插件框架 -- ✅ 文件导出功能 -- ✅ 资源文件自动复制 -- 🚧 Canvas 文件渲染(开发中) +- ✅ Markdown 文件导出功能 +- ✅ 资源文件自动复制和路径更新 +- ✅ Canvas 文件渲染为 PNG 图片 +- ✅ Canvas 透明背景支持 +- ✅ 分块渲染大型 Canvas(避免尺寸限制) +- ✅ 灵活的路径规则配置 -## Canvas 渲染计划 +## 技术特点 -对于 `.canvas` 文件,计划实现的方案: - -1. **方案 A(优先)**:基于 Obsidian 自己的渲染(探索中) -2. **方案 B(备选)**:使用 Headless Browser 手动渲染 JSON Canvas +- **Binary 模式文件操作**:保留原始文件格式(包括行尾空格) +- **智能链接解析**:支持多种链接格式(Markdown、Wikilink、相对路径、绝对路径) +- **分块渲染**:对大型 Canvas 进行分块渲染,避免超过 16284px 的尺寸限制 +- **缩放校正**:考虑系统 DPI 和缩放设置,确保高清输出 +- **Electron 集成**:使用 Obsidian 内部的 Electron API 实现 Canvas 截图 ## 开发 ```bash -cd .obsidian/plugins/vitepress-exporter +cd .obsidian/plugins/odox # 安装依赖 npm install @@ -79,6 +104,8 @@ npm run build ## 注意事项 -- 确保项目中有 `docs/` 目录 +- 确保项目中有配置的目标目录 - 资源文件会被复制,不会移动原文件 - 如果目标文件已存在,会被覆盖 +- Canvas 渲染仅支持桌面版 Obsidian(需要 Electron) +- 生成的 `` 组件需要在静态站点中配置对应组件 diff --git a/manifest.json b/manifest.json index 18b397f..4635f0f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "vitepress-exporter", - "name": "VitePress Exporter", + "id": "odox", + "name": "Odox", "version": "0.1.0", "minAppVersion": "0.15.0", - "description": "Export Obsidian notes to VitePress with asset management", + "description": "Export Obsidian notes to static docs tools with asset management", "author": "nan", "authorUrl": "https://github.com/icrdr", "isDesktopOnly": false diff --git a/notes/implementation.md b/notes/implementation.md index e72c1dd..c2c1072 100644 --- a/notes/implementation.md +++ b/notes/implementation.md @@ -1,9 +1,10 @@ ## 核心功能 -1. **Markdown 文件导出**:将 `.md` 文件导出到 VitePress 的 docs 目录,自动处理资源文件 -2. **Canvas 文件导出**:将 `.canvas` 文件渲染为图片并生成对应的 `.canvas.md` 文件 -3. **资源文件管理**:自动复制和处理图片、Canvas 等资源文件 -4. **路径规则配置**:支持多种目录结构和命名规则 +1. **Markdown 文件导出**:将 `.md` 文件导出到静态文档站点目录,自动处理资源文件 +2. **Canvas 文件导出**:将 `.canvas` 文件渲染为高清 PNG 图片并生成对应的 `.canvas.md` 文件 +3. **资源文件管理**:自动复制和处理图片、Canvas 等资源文件到统一目录 +4. **路径规则配置**:支持多种目录结构和命名规则,灵活配置输出路径 +5. **透明背景支持**:Canvas 导出时可选择透明背景,移除 Obsidian 的点状背景图案 ## 主要流程 @@ -115,7 +116,11 @@ graph TB PrepareCanvas --> DeselectAll[取消选择] DeselectAll --> AddScreenshotting[添加截图样式] - AddScreenshotting --> AppendToBody[附加到 body] + AddScreenshotting --> CheckTransparent{透明背景?} + CheckTransparent -->|是| AddTransparentClass[添加透明样式类] + CheckTransparent -->|否| HideElements[隐藏页面元素] + AddTransparentClass --> HideElements + HideElements --> AppendToBody[附加到 body] AppendToBody --> SetPosition[设置位置] SetPosition --> CalcViewport[计算视口] @@ -143,7 +148,11 @@ graph TB ToUint8Array --> ReturnResult[返回结果] ReturnResult --> Cleanup[清理环境] - Cleanup --> RestoreStyles[恢复样式] + Cleanup --> CheckTransparentCleanup{透明背景?} + CheckTransparentCleanup -->|是| RestoreHiddenElements[恢复隐藏元素] + CheckTransparentCleanup -->|否| RestoreStyles[恢复样式] + RestoreHiddenElements --> RemoveBodyClasses[移除 body 类] + RemoveBodyClasses --> RestoreStyles RestoreStyles --> ResetCanvas[重置 Canvas] ResetCanvas --> End[结束] @@ -172,10 +181,10 @@ graph TB **位置**:main.ts:138-192 **功能**:专门处理 Canvas 文件导出 -1. 渲染 Canvas 为 PNG 图片 -2. 保存图片到 assetsPath(扁平化) +1. 渲染 Canvas 为 PNG 图片(支持透明背景) +2. 保存图片到 assetsPath(扁平化存储) 3. 计算 .canvas.md 文件路径 -4. 生成包含 `` 组件的 Markdown 内容 +4. 使用配置的嵌入模板生成 Markdown 内容 5. 写入文件 **路径规则示例**: @@ -219,6 +228,8 @@ notes/测试用.canvas → docs/测试用.canvas.md - 分块渲染:处理大型 Canvas(避免超过 16284px 限制) - 缩放校正:考虑系统缩放和设备像素比 - 像素级精确:使用 binary 模式保留原始格式 +- 透明背景:通过临时 CSS 类和 DOM 操作实现透明背景导出 +- 智能隐藏:导出时临时隐藏页面其他元素,确保只捕获 Canvas 内容 **分块渲染算法**: ```typescript @@ -262,6 +273,11 @@ for (let row = 0; row < rows; row++) { **转换规则**: ```markdown +// 默认模板:![]({url}) +![](测试用.canvas) → ![](/测试用.canvas.png) +![[测试用.canvas]] → ![](/测试用.canvas.png) + +// 自定义模板: ![](测试用.canvas) → ![[测试用.canvas]] → ``` @@ -271,22 +287,24 @@ for (let row = 0; row < rows; row++) { ### settings.ts ```typescript -interface vitepressExporterSettings { - docsPath: string; // VitePress docs 目录路径,默认 'docs' - assetsPath: string; // 资源目录路径,默认 'docs/public' - nestNoteFolder: boolean; // 是否为每个笔记创建文件夹,默认 true - preserveFolderStructure: boolean; // 是否保留原始文件夹结构,默认 true +interface odoxSettings { + docsPath: string; // 文档站点目录路径,默认 'docs' + assetsPath: string; // 资源目录路径,默认 'docs/public' + nestNoteFolder: boolean; // 是否为每个笔记创建文件夹,默认 true + preserveFolderStructure: boolean; // 是否保留原始文件夹结构,默认 true + transparentCanvas: boolean; // 是否使用透明背景导出 Canvas,默认 true + canvasEmbedTemplate: string; // Canvas 嵌入模板,默认 '![]({url})' } ``` ### 路径规则矩阵 -| preserveFolderStructure | nestNoteFolder | 输入 | 输出 | -|---------|---------|------|------| -| true | true | notes/test.md | docs/notes/test/index.md | -| true | false | notes/test.md | docs/notes/test.md | -| false | true | notes/test.md | docs/test/index.md | -| false | false | notes/test.md | docs/test.md | +| preserveFolderStructure | nestNoteFolder | Markdown 输入 | Markdown 输出 | Canvas 输入 | Canvas 输出 | +|---------|---------|------|------|------|------| +| true | true | notes/test.md | docs/notes/test/index.md | notes/test.canvas | docs/notes/test.canvas/index.md | +| true | false | notes/test.md | docs/notes/test.md | notes/test.canvas | docs/notes/test.md | +| false | true | notes/test.md | docs/test/index.md | notes/test.canvas | docs/test.canvas/index.md | +| false | false | notes/test.md | docs/test.md | notes/test.canvas | docs/test.canvas.md | ## 技术实现要点 @@ -329,21 +347,25 @@ const capturedData = await webContents.capturePage({ x, y, width, height }); ### 5. Canvas 渲染优化 -- **分块渲染**:避免单个 Canvas 过大 +- **分块渲染**:避免单个 Canvas 过大,超过 16284px 限制 - **异步处理**:使用 `requestAnimationFrame` 确保渲染完成 -- **缩放校正**:考虑系统 DPI 设置 +- **缩放校正**:考虑系统 DPI 设置,确保高清输出 - **内存管理**:及时释放临时对象(URL.revokeObjectURL) +- **透明背景**:通过 CSS 类和 DOM 操作实现透明背景导出 +- **智能隐藏**:导出时临时隐藏页面其他元素,确保只捕获 Canvas 内容 ## 文件结构 ``` -.obsidian/plugins/vitepress-exporter/ +.obsidian/plugins/odox/ ├── src/ -│ ├── main.ts # 主插件逻辑(650 行) +│ ├── main.ts # 主插件逻辑(约 680 行) │ └── settings.ts # 设置定义和 UI ├── notes/ # 文档和说明 +│ └── implementation.md # 实现细节文档 ├── manifest.json # 插件清单 -└── package.json # 依赖管理 +├── package.json # 依赖管理 +└── main.js # 编译后的插件文件 ``` ## 依赖项 @@ -354,22 +376,25 @@ const capturedData = await webContents.capturePage({ x, y, width, height }); ## 使用场景 -1. **从 Obsidian 迁移到 VitePress**:将个人知识库转换为静态网站 -2. **技术文档发布**:在 Obsidian 中编写,发布到 VitePress -3. **Canvas 可视化**:将 Obsidian Canvas 导出为交互式图片 -4. **自动化工作流**:结合 CI/CD 实现自动部署 +1. **从 Obsidian 迁移到静态站点**:将个人知识库转换为 VitePress、Docusaurus 等静态网站 +2. **技术文档发布**:在 Obsidian 中编写,发布到文档站点 +3. **Canvas 可视化**:将 Obsidian Canvas 导出为高清 PNG 图片 +4. **自动化工作流**:结合 CI/CD 实现自动部署文档站点 -## 局限性 +## 技术优势 -1. **平台依赖**:Canvas 渲染仅支持桌面版 Obsidian(需要 Electron) -2. **VitePress 特定**:生成的 `` 组件需要在 VitePress 中配置 -3. **链接转换**:复杂的链接语法可能需要手动调整 -4. **大型 Canvas**:分块渲染可能耗时较长 +1. **高质量渲染**:使用 Obsidian 原生渲染引擎,确保导出内容与编辑器中一致 +2. **智能资源管理**:自动识别、复制和更新资源文件引用 +3. **灵活配置**:支持多种目录结构和命名规则,适应不同的项目需求 +4. **透明背景支持**:Canvas 导出时可移除背景图案,适合嵌入各种主题的网站 +5. **分块渲染技术**:处理大型 Canvas 文件,避免尺寸限制 ## 未来改进方向 -1. **增量导出**:只导出修改过的文件 -2. **批量导出**:支持一次性导出整个文件夹 -3. **配置预设**:保存常用的导出配置 -4. **错误恢复**:更完善的错误处理和回滚机制 -5. **性能优化**:并行处理多个文件 +1. **增量导出**:只导出修改过的文件,提高大项目的导出效率 +2. **批量导出**:支持一次性导出整个文件夹或整个项目 +3. **配置预设**:保存常用的导出配置,快速切换不同项目的导出规则 +4. **错误恢复**:更完善的错误处理和回滚机制,导出失败时恢复原状态 +5. **性能优化**:并行处理多个文件,利用多核 CPU 加速导出过程 +6. **进度显示**:为批量导出操作提供进度条和详细的状态信息 +7. **导出历史**:记录导出历史,支持撤销和回滚操作 diff --git a/package.json b/package.json index e7f091d..af15d3f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "vitepress-exporter", + "name": "odox", "version": "0.1.0", - "description": "Export Obsidian notes to VitePress", + "description": "Export Obsidian notes to static docs tools", "main": "main.js", "type": "module", "scripts": { diff --git a/src/main.ts b/src/main.ts index 2fd2062..dfa2d8e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,18 @@ import { Plugin, TFile, Notice, Menu, MenuItem } from 'obsidian'; -import { DEFAULT_SETTINGS, vitepressExporterSettings, vitepressExporterSettingTab } from "./settings"; +import { DEFAULT_SETTINGS, odoxSettings, odoxSettingTab } from "./settings"; -export default class vitepressExporterPlugin extends Plugin { - settings!: vitepressExporterSettings; +export default class odoxPlugin extends Plugin { + settings!: odoxSettings; async onload() { await this.loadSettings(); // 添加导出命令 this.addCommand({ - id: 'export-to-vitepress', - name: 'Export current note to vitepress', + id: 'export-to-docs', + name: 'Export current note to docs', callback: () => { void this.exportCurrentNote(); } @@ -27,7 +27,7 @@ export default class vitepressExporterPlugin extends Plugin { menu.addItem((item: MenuItem) => { item - .setTitle("Export to vitepress") + .setTitle("Export to docs") .setIcon("upload") .onClick(() => { void this.exportNote(file); @@ -43,7 +43,7 @@ export default class vitepressExporterPlugin extends Plugin { this.app.workspace.on("editor-menu", (menu: Menu) => { menu.addItem((item: MenuItem) => { item - .setTitle("Export to vitepress") + .setTitle("Export to docs") .setIcon("upload") .onClick(() => { void this.exportCurrentNote(); @@ -53,13 +53,13 @@ export default class vitepressExporterPlugin extends Plugin { ); // 添加设置面板 - this.addSettingTab(new vitepressExporterSettingTab(this.app, this)); + this.addSettingTab(new odoxSettingTab(this.app, this)); - console.warn('vitepress Exporter plugin loaded'); + console.warn('Odox plugin loaded'); } onunload() { - console.warn('vitepress Exporter plugin unloaded'); + console.warn('Odox plugin unloaded'); } async loadSettings() { @@ -155,7 +155,6 @@ export default class vitepressExporterPlugin extends Plugin { // 写入渲染的图片 await vault.adapter.writeBinary(imagePath, renderedImage.buffer); - console.debug(`Successfully rendered canvas: ${file.path} -> ${imagePath}`); // 3. 计算 .canvas.md 文件的输出路径(复用现有逻辑) const fileNameWithoutExt = file.name.replace('.canvas', ''); @@ -181,7 +180,7 @@ export default class vitepressExporterPlugin extends Plugin { await vault.adapter.mkdir(targetDir); // 5. 生成 .canvas.md 内容 - const content = ``; + const content = this.settings.canvasEmbedTemplate.replace('{url}', `/${imageFileName}`); // 6. 写入 .canvas.md 文件 const contentBuffer = new TextEncoder().encode(content); @@ -195,17 +194,9 @@ export default class vitepressExporterPlugin extends Plugin { async processAssets(content: string, sourceFile: TFile): Promise { const { vault, metadataCache } = this.app; - console.debug('Processing assets for file:', sourceFile.path); - // 使用 Obsidian API 获取文件的所有嵌入和链接 const cache = metadataCache.getCache(sourceFile.path); - - console.debug('Cache:', cache); - console.debug('Embeds:', cache?.embeds); - console.debug('Links:', cache?.links); - if (!cache) { - console.debug('No cache found for file'); return content; } @@ -215,9 +206,7 @@ export default class vitepressExporterPlugin extends Plugin { // 收集嵌入文件(图片等) if (cache.embeds) { for (const embed of cache.embeds) { - console.debug('Processing embed:', embed); const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path); - console.debug('Resolved link for embed:', resolvedLink?.path); if (resolvedLink) { // 检查是否是 canvas 文件 if (resolvedLink.extension === 'canvas') { @@ -240,10 +229,8 @@ export default class vitepressExporterPlugin extends Plugin { } } - console.debug('Assets to copy:', Array.from(assetsToCopy)); - console.debug('Canvas files to render:', Array.from(canvasFiles)); - - // 处理 Canvas 文件:渲染为图片 + // 处理 Canvas 文件:渲染为图片,并收集映射关系 + const canvasImageMap = new Map(); for (const canvasPath of canvasFiles) { try { const canvasFile = vault.getAbstractFileByPath(canvasPath); @@ -260,25 +247,24 @@ export default class vitepressExporterPlugin extends Plugin { // 写入渲染的图片 await vault.adapter.writeBinary(imagePath, renderedImage.buffer); - // 更新内容中的 canvas 引用,使用相对路径 assets/xxx.png - content = this.updateCanvasLinksInContent(content, canvasFile, canvasPath, imageFileName); + // 收集 canvas 文件到图片的映射关系 + canvasImageMap.set(canvasPath, imageFileName); - console.debug(`Successfully rendered canvas: ${canvasPath} -> ${imagePath}`); } } } catch (error) { console.error(`Failed to render canvas ${canvasPath}:`, error); } } + - const activeLeaf = this.app.workspace.getLeaf(); - await activeLeaf.openFile(sourceFile); + // 统一更新所有 canvas 引用 + content = this.updateCanvasInContent(content, canvasImageMap, sourceFile); // 复制资源文件到 docs/assets for (const assetPath of assetsToCopy) { try { const sourceFile = vault.getAbstractFileByPath(assetPath); - console.debug('Source file for asset:', sourceFile); if (sourceFile instanceof TFile) { // 使用 binary 读取模式来保留文件原始内容 @@ -286,7 +272,6 @@ export default class vitepressExporterPlugin extends Plugin { const fileName = sourceFile.name; const destPath = `${this.settings.assetsPath}/${fileName}`; - console.debug('Copying asset:', assetPath, '->', destPath); // 确保目标目录存在 await vault.adapter.mkdir(this.settings.assetsPath); @@ -297,7 +282,6 @@ export default class vitepressExporterPlugin extends Plugin { // 更新内容中的资源路径(使用更智能的替换) content = this.updateLinksInContent(content, sourceFile, assetPath, fileName); - console.debug(`Successfully copied asset: ${assetPath} -> ${destPath}`); } } catch (error) { console.error(`Failed to copy asset ${assetPath}:`, error); @@ -327,7 +311,6 @@ export default class vitepressExporterPlugin extends Plugin { encodeURI(sourceFile.name) ]; - console.debug('Trying to replace links for paths:', possiblePaths); // 使用根路径: /image.png const assetsRef = `/${newFileName}`; @@ -338,22 +321,20 @@ export default class vitepressExporterPlugin extends Plugin { // 检查这个路径是否匹配我们的目标文件 for (const testPath of possiblePaths) { if (path === testPath || path === testPath.replace(/^\//, '')) { - console.debug(`Found matching link: ${path} -> ${assetsRef}`); return `![](${assetsRef})`; } } return match; }); - // 替换 wikilink 格式: ![[path]] -> ![[assets/newfile.png]] - const wikilinkRegex = /!\[\[([^\]]+)\]\]/g; - content = content.replace(wikilinkRegex, (match, link: string) => { + // 替换 docslink 格式: ![[path]] -> ![[assets/newfile.png]] + const docslinkRegex = /!\[\[([^\]]+)\]\]/g; + content = content.replace(docslinkRegex, (match, link: string) => { // 移除可能的别名字段 const cleanLink = link.split('|')[0]!.trim(); for (const testPath of possiblePaths) { if (cleanLink === testPath || cleanLink === testPath.replace(/^\//, '')) { - console.debug(`Found matching wikilink: ${cleanLink} -> ${assetsRef}`); return `![[${assetsRef}]]`; } } @@ -375,13 +356,11 @@ export default class vitepressExporterPlugin extends Plugin { //@ts-ignore const canvas = activeLeaf.view.canvas; if (!canvas) { - console.debug("Could not get canvas object from view"); return null; } - // 选项设置(基于用户要求) - const zoomScale = 1.0; // 缩放比例 1.0 (100%) - const privacyMode = false; // 不启用隐私模式 + // 选项设置 + const zoomScale = 1.0; try { // 获取系统参数 @@ -407,7 +386,6 @@ export default class vitepressExporterPlugin extends Plugin { const h = Math.min(2, 16284 / s / c, 16284 / s / u); const d = Math.min(zoomScale, h); // 实际使用的缩放比例 - console.debug(`Canvas export: scale=${d}, size=${c}x${u}`); // Full canvas 模式 - 使用分块渲染 const e = canvas.wrapperEl; @@ -415,11 +393,28 @@ export default class vitepressExporterPlugin extends Plugin { // 准备 Canvas 环境 this.deselectAll(); e.addClass("is-screenshotting"); - if (privacyMode) { - e.addClass("is-text-garbled"); + // 如果设置了透明背景,添加自定义 CSS 类 + if (this.settings.transparentCanvas) { + e.addClass("canvas-transparent-export"); } // @ts-ignore canvas.screenshotting = true; + + // 临时隐藏页面的其他元素,以便透明导出 + const hiddenElements: HTMLElement[] = []; + if (this.settings.transparentCanvas) { + // 隐藏 body 的所有直接子元素(除了当前 canvas) + Array.from(document.body.children).forEach((child) => { + if (child !== e && child instanceof HTMLElement) { + child.classList.add('odox-hidden'); + hiddenElements.push(child); + } + }); + + // 临时设置 body 样式 + document.body.classList.add('odox-capturing'); + } + document.body.appendChild(e); try { @@ -462,7 +457,6 @@ export default class vitepressExporterPlugin extends Plugin { const startX = bbox.minX + tileWidth / 2; const startY = bbox.minY + tileHeight / 2; - console.debug(`Rendering ${cols}x${rows} tiles (${cols * rows} total)`); // 创建最终 Canvas const finalCanvas = document.createElement("canvas"); @@ -510,7 +504,6 @@ export default class vitepressExporterPlugin extends Plugin { URL.revokeObjectURL(imageUrl); tileIndex++; - console.debug(`Rendered tile ${tileIndex}/${totalTiles}`); } } @@ -527,10 +520,18 @@ export default class vitepressExporterPlugin extends Plugin { } finally { // 清理 + // 恢复隐藏的页面元素 + if (this.settings.transparentCanvas) { + document.body.classList.remove('odox-capturing'); + hiddenElements.forEach((el) => { + el.classList.remove('odox-hidden'); + }); + } + // @ts-ignore this.app.workspace.getLeaf().view?.contentEl?.appendChild(e); e.removeClass("is-screenshotting"); - e.removeClass("is-text-garbled"); + e.removeClass("canvas-transparent-export"); e.setCssStyles({ top: "", left: "", @@ -589,56 +590,83 @@ export default class vitepressExporterPlugin extends Plugin { } } - // 更新内容中的 Canvas 链接 - updateCanvasLinksInContent(content: string, canvasFile: TFile, canvasPath: string, imageFileName: string): string { + // 统一更新所有 Canvas 链接 + updateCanvasInContent(content: string, canvasImageMap: Map, sourceFile: TFile): string { // 获取源文件的目录路径 - const sourceDir = canvasFile.path.substring(0, canvasFile.path.lastIndexOf('/')); + const sourceDir = sourceFile.path.substring(0, sourceFile.path.lastIndexOf('/')); if (!sourceDir) return content; - // 尝试多种可能的路径格式 - const possiblePaths = [ - // 完整路径 - canvasPath, - // 相对于源文件的路径 - canvasPath.substring(sourceDir.length + 1), - // URL 编码版本 - encodeURI(canvasPath), - encodeURI(canvasPath.substring(sourceDir.length + 1)), - // 文件名(包括和不包括 .canvas 扩展名) - canvasFile.name, - canvasFile.name.replace('.canvas', ''), - encodeURI(canvasFile.name), - encodeURI(canvasFile.name.replace('.canvas', '')) - ]; + // 使用根路径: /filename.png + const assetsRef = (imageFileName: string) => `/${imageFileName}`; - console.debug('Trying to replace canvas links for paths:', possiblePaths); - - // 使用根路径: /image.png - const assetsRef = `/${imageFileName}`; - - // 替换 markdown 格式: !(canvas) -> + // 替换 markdown 格式: !(canvas) -> 模板 const markdownRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; content = content.replace(markdownRegex, (match, _alt, path) => { - // 检查这个路径是否匹配我们的 canvas 文件 - for (const testPath of possiblePaths) { - if (path === testPath || path === testPath.replace(/^\//, '')) { - console.debug(`Found matching canvas link: ${path} -> `); - return ``; + // 尝试为这个路径找到对应的 canvas 文件 + for (const [canvasPath, imageFileName] of canvasImageMap) { + const canvasFile = this.app.vault.getAbstractFileByPath(canvasPath); + if (canvasFile instanceof TFile) { + // 构建可能的路径格式 + const possiblePaths = [ + // 完整路径 + canvasPath, + // 相对于源文件的路径 + canvasPath.substring(sourceDir.length + 1), + // URL 编码版本 + encodeURI(canvasPath), + encodeURI(canvasPath.substring(sourceDir.length + 1)), + // 文件名(包括和不包括 .canvas 扩展名) + canvasFile.name, + canvasFile.name.replace('.canvas', ''), + encodeURI(canvasFile.name), + encodeURI(canvasFile.name.replace('.canvas', '')) + ]; + + // 检查是否匹配 + for (const testPath of possiblePaths) { + if (path === testPath || path === testPath.replace(/^\//, '')) { + const replacement = this.settings.canvasEmbedTemplate.replace("{url}", assetsRef(imageFileName)); + return replacement; + } + } } } return match; }); - // 替换 wikilink 格式: ![[canvas]] -> - const wikilinkRegex = /!\[\[([^\]]+)\]\]/g; - content = content.replace(wikilinkRegex, (match, link) => { + // 替换 docslink 格式: ![[canvas]] -> 模板 + const docslinkRegex = /!\[\[([^\]]+)\]\]/g; + content = content.replace(docslinkRegex, (match, link) => { // 移除可能的别名字段 - const cleanLink = link.split('|')[0].trim(); + const cleanLink = link.split('|')[0]!.trim(); - for (const testPath of possiblePaths) { - if (cleanLink === testPath || cleanLink === testPath.replace(/^\//, '')) { - console.debug(`Found matching canvas wikilink: ${cleanLink} -> `); - return ``; + // 尝试为这个链接找到对应的 canvas 文件 + for (const [canvasPath, imageFileName] of canvasImageMap) { + const canvasFile = this.app.vault.getAbstractFileByPath(canvasPath); + if (canvasFile instanceof TFile) { + // 构建可能的路径格式 + const possiblePaths = [ + // 完整路径 + canvasPath, + // 相对于源文件的路径 + canvasPath.substring(sourceDir.length + 1), + // URL 编码版本 + encodeURI(canvasPath), + encodeURI(canvasPath.substring(sourceDir.length + 1)), + // 文件名(包括和不包括 .canvas 扩展名) + canvasFile.name, + canvasFile.name.replace('.canvas', ''), + encodeURI(canvasFile.name), + encodeURI(canvasFile.name.replace('.canvas', '')) + ]; + + // 检查是否匹配 + for (const testPath of possiblePaths) { + if (cleanLink === testPath || cleanLink === testPath.replace(/^\//, '')) { + const replacement = this.settings.canvasEmbedTemplate.replace("{url}", assetsRef(imageFileName)); + return replacement; + } + } } } return match; diff --git a/src/settings.ts b/src/settings.ts index 4a0622b..4dafb46 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,25 +1,29 @@ import {App, PluginSettingTab, Setting} from "obsidian"; -import vitepressExporterPlugin from "./main"; +import odoxPlugin from "./main"; -export interface vitepressExporterSettings { +export interface odoxSettings { docsPath: string; assetsPath: string; nestNoteFolder: boolean; preserveFolderStructure: boolean; + transparentCanvas: boolean; + canvasEmbedTemplate: string; } -export const DEFAULT_SETTINGS: vitepressExporterSettings = { - docsPath: 'wiki', - assetsPath: 'wiki/public', +export const DEFAULT_SETTINGS: odoxSettings = { + docsPath: 'docs', + assetsPath: 'docs/public', nestNoteFolder: true, - preserveFolderStructure: true + preserveFolderStructure: true, + transparentCanvas: true, + canvasEmbedTemplate: '![]({url})' } // 设置面板 -export class vitepressExporterSettingTab extends PluginSettingTab { - plugin: vitepressExporterPlugin; +export class odoxSettingTab extends PluginSettingTab { + plugin: odoxPlugin; - constructor(app: App, plugin: vitepressExporterPlugin) { + constructor(app: App, plugin: odoxPlugin) { super(app, plugin); this.plugin = plugin; } @@ -30,7 +34,7 @@ export class vitepressExporterSettingTab extends PluginSettingTab { containerEl.empty(); new Setting(containerEl) - .setName('Docs path') + .setName('Wiki path') .setDesc('Path to the docs directory (relative to vault root)') .addText(text => text // eslint-disable-next-line @@ -46,7 +50,7 @@ export class vitepressExporterSettingTab extends PluginSettingTab { .setDesc('Path to the assets directory (relative to vault root)') .addText(text => text // eslint-disable-next-line - .setPlaceholder('docs/assets') + .setPlaceholder('docs/public') .setValue(this.plugin.settings.assetsPath) .onChange(async (value) => { this.plugin.settings.assetsPath = value; @@ -72,5 +76,26 @@ export class vitepressExporterSettingTab extends PluginSettingTab { this.plugin.settings.preserveFolderStructure = value; await this.plugin.saveSettings(); })); + + new Setting(containerEl) + .setName('Transparent canvas background') + .setDesc('Export canvas with transparent background (hide dots pattern)') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.transparentCanvas) + .onChange(async (value) => { + this.plugin.settings.transparentCanvas = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName('Canvas embed template') + .setDesc('Template for embedding canvas images. Use {url} as placeholder for the image path. Examples: ![]({url}) or ') + .addText(text => text + .setPlaceholder('![]({url})') + .setValue(this.plugin.settings.canvasEmbedTemplate) + .onChange(async (value) => { + this.plugin.settings.canvasEmbedTemplate = value; + await this.plugin.saveSettings(); + })); } } diff --git a/styles.css b/styles.css index 71cc60f..7996a77 100644 --- a/styles.css +++ b/styles.css @@ -6,3 +6,25 @@ available in the app when your plugin is enabled. If your plugin does not need CSS, delete this file. */ + +/* 透明导出模式:隐藏洞洞板背景 */ +.canvas-transparent-export .canvas-background { + display: none !important; +} + +/* 透明导出模式:确保 canvas 容器背景透明 */ +.canvas-transparent-export { + background-color: transparent !important; + background-image: none !important; +} + +/* 截图时隐藏页面其他元素 */ +.odox-hidden { + display: none !important; +} + +/* 截图时的 body 样式 */ +.odox-capturing { + background-color: transparent !important; + background-image: none !important; +}