feat: Rename plugin to Odox and enhance documentation for static site export features
This commit is contained in:
202
src/main.ts
202
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 = `<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 ``;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -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: ''
|
||||
}
|
||||
|
||||
// 设置面板
|
||||
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:  or <Viewer src="{url}" />')
|
||||
.addText(text => text
|
||||
.setPlaceholder('')
|
||||
.setValue(this.plugin.settings.canvasEmbedTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.canvasEmbedTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user