Initial commit: vitepress-exporter plugin

This commit is contained in:
2026-04-20 12:00:50 +08:00
commit e54a8a3b05
15 changed files with 6761 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
main.js
main.js.map
*.log
.DS_Store
versions.json

View File

@@ -0,0 +1,134 @@
// 尝试捕获 Obsidian 原生 Canvas 渲染(通过临时调整视口大小)
async tryNativeCanvasCapture(canvasFile: TFile): Promise<Uint8Array | null> {
let uiStyleElement: HTMLStyleElement | null = null;
let sizeStyleElement: HTMLStyleElement | null = null;
let activeLeaf: any = null;
let canvasWrapper: HTMLElement | null = null;
let originalScrollLeft = 0;
let originalScrollTop = 0;
try {
console.log('🔍 Step 1: Getting active workspace leaf...');
// 获取当前活动的 leaf(不创建新窗口)
activeLeaf = this.app.workspace.activeLeaf;
if (!activeLeaf) {
console.log('❌ No active leaf found');
return null;
}
console.log('🔍 Step 2: Opening canvas file...');
await activeLeaf.openFile(canvasFile);
console.log('⏳ Step 3: Waiting for canvas render...');
await this.waitForCanvasRender();
console.log('🔍 Step 4: Finding canvas DOM...');
const canvasDom = this.findCanvasDomElement();
if (!canvasDom) {
console.log('❌ Could not find canvas DOM element');
return null;
}
console.log('✅ Found canvas DOM:', canvasDom.className, 'Size:', (canvasDom as any).offsetWidth, 'x', (canvasDom as any).offsetHeight);
console.log('🎨 Step 5: Hiding Canvas UI elements...');
uiStyleElement = this.hideCanvasUIElements();
console.log('📐 Step 6: Calculating canvas content size...');
const contentSize = this.getCanvasContentSize(canvasDom);
// 获取 canvasWrapper
if ((canvasDom as HTMLElement).classList.contains('canvas-wrapper')) {
canvasWrapper = canvasDom as HTMLElement;
} else {
canvasWrapper = canvasDom.querySelector('.canvas-wrapper') as HTMLElement;
}
if (!canvasWrapper) {
console.log('❌ Could not find canvas wrapper');
return null;
}
// 保存原始滚动位置
originalScrollLeft = canvasWrapper.scrollLeft;
originalScrollTop = canvasWrapper.scrollTop;
console.log(`📏 Original wrapper size: ${(canvasDom as any).offsetWidth}x${(canvasDom as any).offsetHeight}`);
console.log(`📏 Content size: ${contentSize.width}x${contentSize.height}`);
// 临时调整 canvas wrapper 大小以适应所有内容
console.log('🔧 Temporarily resizing canvas wrapper to fit all content...');
sizeStyleElement = document.createElement('style');
sizeStyleElement.id = 'canvas-export-resize';
sizeStyleElement.textContent = `
/* 临时调整 canvas wrapper 大小 */
.canvas-wrapper {
width: ${contentSize.width + 100}px !important;
height: ${contentSize.height + 100}px !important;
max-width: ${contentSize.width + 100}px !important;
max-height: ${contentSize.height + 100}px !important;
overflow: visible !important;
}
/* 确保画布内容区域也扩展 */
.canvas-view-content {
width: ${contentSize.width + 100}px !important;
height: ${contentSize.height + 100}px !important;
}
`;
document.head.appendChild(sizeStyleElement);
// 等待样式应用
await new Promise(resolve => setTimeout(resolve, 100));
// 重置滚动位置
canvasWrapper.scrollLeft = 0;
canvasWrapper.scrollTop = 0;
// 等待布局更新
await new Promise(resolve => setTimeout(resolve, 200));
console.log('📸 Step 7: Capturing canvas...');
// @ts-ignore
const domToImage = require('dom-to-image-more');
const dataUrl = await domToImage.toPng(canvasDom, {
backgroundColor: '#ffffff',
quality: 1,
width: contentSize.width,
height: contentSize.height,
cacheBust: true
});
const imageData = this.dataUrlToUint8Array(dataUrl);
console.log('✅ Capture successful!');
return imageData;
} catch (error) {
console.error('❌ Native canvas capture failed:', error);
return null;
} finally {
// 清理临时调整大小的样式
if (sizeStyleElement && sizeStyleElement.parentNode) {
sizeStyleElement.parentNode.removeChild(sizeStyleElement);
console.log('🧹 Removed temporary resize styles');
}
if (uiStyleElement && uiStyleElement.parentNode) {
uiStyleElement.parentNode.removeChild(uiStyleElement);
console.log('🧹 Cleaned up UI hiding styles');
}
// 恢复滚动位置
if (canvasWrapper) {
canvasWrapper.scrollLeft = originalScrollLeft;
canvasWrapper.scrollTop = originalScrollTop;
console.log('🔄 Restored original scroll position');
}
console.log('🧹 Cleanup complete');
}
}

84
README.md Normal file
View File

@@ -0,0 +1,84 @@
# VitePress Exporter - Obsidian Plugin
## 功能
这个 Obsidian 插件用于自动化发布 Obsidian 笔记到 VitePress 网站:
- ✅ 将当前 markdown 文件导出到 `docs/` 目录
- ✅ 自动复制引用的资源文件到 `docs/assets/` 目录
- ✅ 更新 markdown 中的资源路径
- 🚧 Canvas 文件渲染(待实现)
## 安装
插件已经安装在这个项目的 `.obsidian/plugins/vitepress-exporter/` 目录下。
1. 在 Obsidian 中打开这个项目
2. 进入设置 → 第三方插件
3. 启用 "VitePress Exporter" 插件
## 使用
### 方式 1:命令面板
1. 打开一个 markdown 文件
2. 按 `Ctrl+P` (或 `Cmd+P`) 打开命令面板
3. 输入 "Export to VitePress"
4. 选择 "Export current note to VitePress"
### 方式 2:功能区按钮
- 点击左侧功能区栏的上传图标按钮
### 方式 3:快捷键
- 可以在 Obsidian 的快捷键设置中为导出命令设置快捷键
## 配置
在插件设置中可以配置:
- **Docs path**: 导出的 markdown 文件存放目录(默认:`docs`)
- **Assets path**: 资源文件存放目录(默认:`docs/assets`)
## 工作流程
1. 用户在 Obsidian 中编写笔记
2. 笔记中引用的资源(图片、PDF 等)可以放在任意位置
3. 点击导出按钮
4. 插件自动:
- 将 markdown 文件复制到 `docs/` 目录
- 扫描并复制所有引用的资源到 `docs/assets/` 目录
- 更新 markdown 中的资源路径
## 当前状态
- ✅ 基础插件框架
- ✅ 文件导出功能
- ✅ 资源文件自动复制
- 🚧 Canvas 文件渲染(开发中)
## Canvas 渲染计划
对于 `.canvas` 文件,计划实现的方案:
1. **方案 A(优先)**:基于 Obsidian 自己的渲染(探索中)
2. **方案 B(备选)**:使用 Headless Browser 手动渲染 JSON Canvas
## 开发
```bash
cd .obsidian/plugins/vitepress-exporter
# 安装依赖
npm install
# 开发模式(自动重新编译)
npm run dev
# 生产构建
npm run build
```
## 注意事项
- 确保项目中有 `docs/` 目录
- 资源文件会被复制,不会移动原文件
- 如果目标文件已存在,会被覆盖

19
docs/.vuepress/config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defaultTheme } from '@vuepress/theme-default';
import type { DefaultThemeOptions } from '@vuepress/theme-default';
import type { PluginOptions } from 'vuepress';
export default {
title: 'VitePress Demo',
description: 'Demo for VitePress with Canvas Viewer',
theme: defaultTheme<DefaultThemeOptions>({
navbar: [
{
text: 'Home',
link: '/',
},
],
}),
clientConfigFile: 'index.ts',
} satisfies PluginOptions;

8
docs/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineClientConfig } from '@vuepress/client';
import { CanvasViewerPlugin } from './plugins/canvas-viewer';
export default defineClientConfig({
enhance({ app, router }) {
app.use(CanvasViewerPlugin);
}
});

View File

@@ -0,0 +1,7 @@
import type { Plugin } from 'vue';
export const CanvasViewerPlugin: Plugin = {
install(app) {
app.component('CanvasViewer', () => import('./components/CanvasViewer.vue'));
}
};

76
esbuild.config.mjs Normal file
View File

@@ -0,0 +1,76 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === 'production');
if (prod) {
console.log('Building production bundle...');
await esbuild.build({
banner: {
js: banner,
},
entryPoints: ['main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins],
format: 'cjs',
target: 'es2018',
logLevel: "info",
sourcemap: false,
treeShaking: true,
outfile: 'main.js',
});
console.log('Production bundle built successfully!');
} else {
const ctx = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ['main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins],
format: 'cjs',
target: 'es2018',
logLevel: "info",
sourcemap: 'inline',
treeShaking: true,
outfile: 'main.js',
});
await ctx.watch();
}

1210
main.ts Normal file

File diff suppressed because it is too large Load Diff

1145
main.ts.backup Normal file

File diff suppressed because it is too large Load Diff

10
manifest.json Normal file
View File

@@ -0,0 +1,10 @@
{
"id": "vitepress-exporter",
"name": "VitePress Exporter",
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Export Obsidian notes to VitePress with asset management",
"author": "Your Name",
"authorUrl": "https://github.com/yourusername",
"isDesktopOnly": false
}

2466
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "vitepress-exporter",
"version": "0.1.0",
"description": "Export Obsidian notes to VitePress",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"vitepress",
"export"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"dom-to-image-more": "^3.7.2",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"dom-to-image-more": "^3.7.2"
}
}

1443
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

92
puppeteer_version.txt Normal file
View File

@@ -0,0 +1,92 @@
// 尝试使用 Puppeteer 截图 Canvas(保留完整样式)
async tryNativeCanvasCapture(canvasFile: TFile): Promise<Uint8Array | null> {
let newWindowLeaf: any = null;
try {
console.log('🔍 Step 1: Opening canvas in new window...');
// 创建新窗口
newWindowLeaf = this.app.workspace.getLeaf('window');
if (!newWindowLeaf) {
console.log('❌ Failed to create new window');
return null;
}
console.log('✅ New window created, opening canvas file...');
await newWindowLeaf.openFile(canvasFile);
console.log('⏳ Step 2: Waiting for canvas render...');
// 等待 Canvas 渲染
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('📸 Step 3: Capturing with Puppeteer...');
// @ts-ignore
const puppeteer = require('puppeteer');
// 启动 Puppeteer
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
// 获取新窗口的 HTML 内容
const container = newWindowLeaf.containerEl;
const canvasWrapper = container.querySelector('.canvas-wrapper');
if (!canvasWrapper) {
console.log('❌ Could not find canvas wrapper');
return null;
}
// 获取完整的 HTML,包括所有样式
const htmlContent = canvasWrapper.outerHTML;
// 设置 HTML 内容
await page.setContent(htmlContent, {
waitUntil: 'networkidle0',
timeout: 10000
});
// 计算内容尺寸
const contentSize = this.getCanvasContentSize(canvasWrapper);
const viewportWidth = Math.ceil(contentSize.width);
const viewportHeight = Math.ceil(contentSize.height);
console.log(`📸 Screenshot size: ${viewportWidth}x${viewportHeight}`);
// 截图
const screenshot = await page.screenshot({
type: 'png',
clip: {
x: contentSize.offsetX,
y: contentSize.offsetY,
width: contentSize.width,
height: contentSize.height
}
});
console.log('✅ Capture successful!');
return new Uint8Array(screenshot);
} finally {
await browser.close();
}
} catch (error) {
console.error('❌ Puppeteer capture failed:', error);
return null;
} finally {
// 关闭新窗口
if (newWindowLeaf) {
try {
newWindowLeaf.detach();
console.log('🧹 Closed new window');
} catch (e) {
console.log('⚠️ Could not close new window:', e);
}
}
}
}

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules",
"docs",
"**/*.vue"
]
}