feat: Rename plugin to Odox and enhance documentation for static site export features

This commit is contained in:
2026-04-22 11:08:20 +08:00
parent f0ca3776f0
commit 0e051b7293
7 changed files with 298 additions and 171 deletions

View File

@@ -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 = `<Viewer src="/${imageFileName}" />`;
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<string> {
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<string, string>();
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<string, string>, 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) -> <Viewer src="/image.png" />
// 替换 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} -> <Viewer src="${assetsRef}" />`);
return `<Viewer src="${assetsRef}" />`;
// 尝试为这个路径找到对应的 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]] -> <Viewer src="/image.png" />
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} -> <Viewer src="${assetsRef}" />`);
return `<Viewer src="${assetsRef}" />`;
// 尝试为这个链接找到对应的 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;

View File

@@ -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 <Viewer src="{url}" />')
.addText(text => text
.setPlaceholder('![]({url})')
.setValue(this.plugin.settings.canvasEmbedTemplate)
.onChange(async (value) => {
this.plugin.settings.canvasEmbedTemplate = value;
await this.plugin.saveSettings();
}));
}
}