- Added support for exporting `.canvas` files by rendering them to images and generating corresponding `.canvas.md` files. - Updated the export logic to handle both Markdown and Canvas files, including file type checks and notifications. - Improved resource management by ensuring assets are copied correctly and links are updated in the exported content. - Refactored settings to change default export paths from 'docs' to 'wiki' for better organization. - Created detailed implementation notes and flowcharts to document the export processes and core functionalities.
650 lines
20 KiB
TypeScript
650 lines
20 KiB
TypeScript
import { Plugin, TFile, Notice, Menu, MenuItem } from 'obsidian';
|
||
import { DEFAULT_SETTINGS, vitepressExporterSettings, vitepressExporterSettingTab } from "./settings";
|
||
|
||
|
||
|
||
export default class vitepressExporterPlugin extends Plugin {
|
||
settings!: vitepressExporterSettings;
|
||
|
||
async onload() {
|
||
await this.loadSettings();
|
||
|
||
// 添加导出命令
|
||
this.addCommand({
|
||
id: 'export-to-vitepress',
|
||
name: 'Export current note to vitepress',
|
||
callback: () => {
|
||
void this.exportCurrentNote();
|
||
}
|
||
});
|
||
|
||
// Add file menu item
|
||
|
||
this.registerEvent(
|
||
// @ts-ignore
|
||
this.app.workspace.on("file-menu", (menu: Menu, file: TFile) => {
|
||
if (file.extension === "md" || file.extension === "canvas") {
|
||
|
||
menu.addItem((item: MenuItem) => {
|
||
item
|
||
.setTitle("Export to vitepress")
|
||
.setIcon("upload")
|
||
.onClick(() => {
|
||
void this.exportNote(file);
|
||
});
|
||
});
|
||
}
|
||
})
|
||
);
|
||
|
||
// Add editor menu item
|
||
this.registerEvent(
|
||
// @ts-ignore
|
||
this.app.workspace.on("editor-menu", (menu: Menu) => {
|
||
menu.addItem((item: MenuItem) => {
|
||
item
|
||
.setTitle("Export to vitepress")
|
||
.setIcon("upload")
|
||
.onClick(() => {
|
||
void this.exportCurrentNote();
|
||
});
|
||
});
|
||
})
|
||
);
|
||
|
||
// 添加设置面板
|
||
this.addSettingTab(new vitepressExporterSettingTab(this.app, this));
|
||
|
||
console.warn('vitepress Exporter plugin loaded');
|
||
}
|
||
|
||
onunload() {
|
||
console.warn('vitepress Exporter plugin unloaded');
|
||
}
|
||
|
||
async loadSettings() {
|
||
// eslint-disable-next-line
|
||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||
}
|
||
|
||
async saveSettings() {
|
||
await this.saveData(this.settings);
|
||
}
|
||
|
||
// 导出当前笔记
|
||
async exportCurrentNote() {
|
||
const activeFile = this.app.workspace.getActiveFile();
|
||
|
||
if (!activeFile) {
|
||
new Notice('No active file found');
|
||
return;
|
||
}
|
||
|
||
if (activeFile.extension !== 'md' && activeFile.extension !== 'canvas') {
|
||
new Notice('Current file is not a Markdown or canvas file');
|
||
return;
|
||
}
|
||
|
||
await this.exportNote(activeFile);
|
||
}
|
||
|
||
// 导出单个笔记
|
||
async exportNote(file: TFile) {
|
||
if (file.extension === 'canvas') {
|
||
await this.exportCanvasFile(file);
|
||
return;
|
||
}
|
||
|
||
const { vault } = this.app;
|
||
|
||
// 使用 binary 模式读取文件以保留原始格式(包括行尾空格)
|
||
const contentBuffer = await vault.readBinary(file);
|
||
const content = new TextDecoder().decode(contentBuffer);
|
||
|
||
// 1. 扫描并复制所有资源文件(传入文件对象用于路径解析)
|
||
const updatedContent = await this.processAssets(content, file);
|
||
|
||
// 2. 计算输出路径
|
||
let docsPath: string;
|
||
const fileNameWithoutExt = file.name.replace('.md', '');
|
||
const filePathWithoutExt = file.path.replace('.md', '');
|
||
|
||
if (this.settings.preserveFolderStructure && this.settings.nestNoteFolder) {
|
||
// 保留文件夹 + 嵌套: notes/some-note.md > docs/notes/some-note/index.md
|
||
docsPath = `${this.settings.docsPath}/${filePathWithoutExt}/index.md`;
|
||
} else if (this.settings.preserveFolderStructure) {
|
||
// 保留文件夹结构: notes/some-note.md > docs/notes/some-note.md
|
||
docsPath = `${this.settings.docsPath}/${file.path}`;
|
||
} else if (this.settings.nestNoteFolder) {
|
||
// 嵌套文件夹: notes/some-note.md > docs/some-note/index.md
|
||
docsPath = `${this.settings.docsPath}/${fileNameWithoutExt}/index.md`;
|
||
} else {
|
||
// 默认: notes/some-note.md > docs/some-note.md
|
||
docsPath = `${this.settings.docsPath}/${file.name}`;
|
||
}
|
||
|
||
// 3. 确保目标目录存在
|
||
const targetDir = docsPath.substring(0, docsPath.lastIndexOf('/'));
|
||
await vault.adapter.mkdir(targetDir);
|
||
|
||
// 4. 写入到 docs 目录(使用 binary 模式保留原始格式)
|
||
const updatedBuffer = new TextEncoder().encode(updatedContent);
|
||
await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer);
|
||
|
||
console.warn(`Exported ${file.name} to ${docsPath}`);
|
||
}
|
||
|
||
// 导出 canvas 文件
|
||
async exportCanvasFile(file: TFile) {
|
||
const { vault } = this.app;
|
||
|
||
// 1. 渲染 canvas 为图片
|
||
const renderedImage = await this.renderCanvas(file);
|
||
if (!renderedImage) {
|
||
new Notice(`Failed to render canvas: ${file.name}`);
|
||
return;
|
||
}
|
||
|
||
// 2. 保存图片到 assetsPath (扁平化存储)
|
||
const imageFileName = file.name + '.png';
|
||
const imagePath = `${this.settings.assetsPath}/${imageFileName}`;
|
||
|
||
// 确保目标目录存在
|
||
await vault.adapter.mkdir(this.settings.assetsPath);
|
||
|
||
// 写入渲染的图片
|
||
await vault.adapter.writeBinary(imagePath, renderedImage.buffer);
|
||
|
||
console.debug(`Successfully rendered canvas: ${file.path} -> ${imagePath}`);
|
||
|
||
// 3. 计算 .canvas.md 文件的输出路径(复用现有逻辑)
|
||
const fileNameWithoutExt = file.name.replace('.canvas', '');
|
||
const filePathWithoutExt = file.path.replace('.canvas', '');
|
||
|
||
let docsPath: string;
|
||
if (this.settings.preserveFolderStructure && this.settings.nestNoteFolder) {
|
||
// 保留文件夹 + 嵌套: notes/some-note.canvas > docs/notes/some-note.canvas/index.md
|
||
docsPath = `${this.settings.docsPath}/${filePathWithoutExt}.canvas/index.md`;
|
||
} else if (this.settings.preserveFolderStructure) {
|
||
// 保留文件夹结构: notes/some-note.canvas > docs/notes/some-note.canvas.md
|
||
docsPath = `${this.settings.docsPath}/${filePathWithoutExt}.md`;
|
||
} else if (this.settings.nestNoteFolder) {
|
||
// 嵌套文件夹: notes/some-note.canvas > docs/some-note.canvas/index.md
|
||
docsPath = `${this.settings.docsPath}/${fileNameWithoutExt}.canvas/index.md`;
|
||
} else {
|
||
// 默认: notes/some-note.canvas > docs/some-note.canvas.md
|
||
docsPath = `${this.settings.docsPath}/${fileNameWithoutExt}.canvas.md`;
|
||
}
|
||
|
||
// 4. 确保 .canvas.md 目标目录存在
|
||
const targetDir = docsPath.substring(0, docsPath.lastIndexOf('/'));
|
||
await vault.adapter.mkdir(targetDir);
|
||
|
||
// 5. 生成 .canvas.md 内容
|
||
const content = `<Viewer src="/${imageFileName}" />`;
|
||
|
||
// 6. 写入 .canvas.md 文件
|
||
const contentBuffer = new TextEncoder().encode(content);
|
||
await vault.adapter.writeBinary(docsPath, contentBuffer.buffer);
|
||
|
||
console.warn(`Exported ${file.name} to ${docsPath}`);
|
||
new Notice(`Exported ${file.name} to ${docsPath}`);
|
||
}
|
||
|
||
// 处理资源文件
|
||
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;
|
||
}
|
||
|
||
const assetsToCopy = new Set<string>();
|
||
const canvasFiles = new Set<string>();
|
||
|
||
// 收集嵌入文件(图片等)
|
||
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') {
|
||
canvasFiles.add(resolvedLink.path);
|
||
} else {
|
||
assetsToCopy.add(resolvedLink.path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收集普通链接(可选,如果也想复制链接的文件)
|
||
if (cache.links) {
|
||
for (const link of cache.links) {
|
||
const resolvedLink = metadataCache.getFirstLinkpathDest(link.link, sourceFile.path);
|
||
// 只复制本地文件链接,不包括其他 md 文件
|
||
if (resolvedLink && resolvedLink.extension !== 'md') {
|
||
assetsToCopy.add(resolvedLink.path);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.debug('Assets to copy:', Array.from(assetsToCopy));
|
||
console.debug('Canvas files to render:', Array.from(canvasFiles));
|
||
|
||
// 处理 Canvas 文件:渲染为图片
|
||
for (const canvasPath of canvasFiles) {
|
||
try {
|
||
const canvasFile = vault.getAbstractFileByPath(canvasPath);
|
||
if (canvasFile instanceof TFile) {
|
||
const renderedImage = await this.renderCanvas(canvasFile);
|
||
if (renderedImage) {
|
||
const imageFileName = canvasFile.name + '.png';
|
||
// 图片保存到 docs/assets/
|
||
const imagePath = `${this.settings.assetsPath}/${imageFileName}`;
|
||
|
||
// 确保目标目录存在
|
||
await vault.adapter.mkdir(this.settings.assetsPath);
|
||
|
||
// 写入渲染的图片
|
||
await vault.adapter.writeBinary(imagePath, renderedImage.buffer);
|
||
|
||
// 更新内容中的 canvas 引用,使用相对路径 assets/xxx.png
|
||
content = this.updateCanvasLinksInContent(content, canvasFile, 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);
|
||
|
||
// 复制资源文件到 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 读取模式来保留文件原始内容
|
||
const assetContent = await vault.readBinary(sourceFile);
|
||
const fileName = sourceFile.name;
|
||
const destPath = `${this.settings.assetsPath}/${fileName}`;
|
||
|
||
console.debug('Copying asset:', assetPath, '->', destPath);
|
||
|
||
// 确保目标目录存在
|
||
await vault.adapter.mkdir(this.settings.assetsPath);
|
||
|
||
// 写入资源文件(使用 binary 模式)
|
||
await vault.adapter.writeBinary(destPath, assetContent);
|
||
|
||
// 更新内容中的资源路径(使用更智能的替换)
|
||
content = this.updateLinksInContent(content, sourceFile, assetPath, fileName);
|
||
|
||
console.debug(`Successfully copied asset: ${assetPath} -> ${destPath}`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`Failed to copy asset ${assetPath}:`, error);
|
||
}
|
||
}
|
||
|
||
return content;
|
||
}
|
||
|
||
// 更新内容中的链接
|
||
updateLinksInContent(content: string, sourceFile: TFile, assetPath: string, newFileName: string): string {
|
||
// 获取源文件的目录路径
|
||
const sourceDir = sourceFile.path.substring(0, sourceFile.path.lastIndexOf('/'));
|
||
if (!sourceDir) return content;
|
||
|
||
// 尝试多种可能的路径格式
|
||
const possiblePaths = [
|
||
// 完整路径
|
||
assetPath,
|
||
// 相对于源文件的路径
|
||
assetPath.substring(sourceDir.length + 1),
|
||
// URL 编码版本
|
||
encodeURI(assetPath),
|
||
encodeURI(assetPath.substring(sourceDir.length + 1)),
|
||
// 文件名
|
||
sourceFile.name,
|
||
encodeURI(sourceFile.name)
|
||
];
|
||
|
||
console.debug('Trying to replace links for paths:', possiblePaths);
|
||
|
||
// 使用根路径: /image.png
|
||
const assetsRef = `/${newFileName}`;
|
||
|
||
// 替换 markdown 格式:  -> 
|
||
const markdownRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||
content = content.replace(markdownRegex, (match, alt, path) => {
|
||
// 检查这个路径是否匹配我们的目标文件
|
||
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) => {
|
||
// 移除可能的别名字段
|
||
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}]]`;
|
||
}
|
||
}
|
||
return match;
|
||
});
|
||
|
||
return content;
|
||
}
|
||
|
||
// Use Canvas internal API to export (基于 Obsidian export as image 实现)
|
||
/* eslint-disable */
|
||
async renderCanvas(
|
||
canvasFile: TFile
|
||
): Promise<Uint8Array | null> {
|
||
|
||
const activeLeaf = this.app.workspace.getLeaf();
|
||
await activeLeaf.openFile(canvasFile);
|
||
|
||
//@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; // 不启用隐私模式
|
||
|
||
try {
|
||
// 获取系统参数
|
||
const win = window;
|
||
//@ts-ignore
|
||
const electron = win.electron?.remote;
|
||
//@ts-ignore
|
||
const webContents = electron?.getCurrentWebContents();
|
||
//@ts-ignore
|
||
const zoomLevel = webContents?.getZoomLevel() || 0;
|
||
const o = Math.pow(1.2, zoomLevel); // 系统缩放因子
|
||
const a = win.devicePixelRatio || 1; // 设备像素比
|
||
const s = Math.round(a / o * 1e5) / 1e5; // 缩放校正系数
|
||
|
||
// 获取 Canvas 边界
|
||
//@ts-ignore
|
||
const nodes = Array.from(canvas.nodes.values());
|
||
const bbox = this.calculateBBox(nodes);
|
||
const c = bbox.maxX - bbox.minX; // 宽度
|
||
const u = bbox.maxY - bbox.minY; // 高度
|
||
|
||
// 计算最大缩放限制(避免超过 16284px)
|
||
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;
|
||
|
||
// 准备 Canvas 环境
|
||
this.deselectAll();
|
||
e.addClass("is-screenshotting");
|
||
if (privacyMode) {
|
||
e.addClass("is-text-garbled");
|
||
}
|
||
// @ts-ignore
|
||
canvas.screenshotting = true;
|
||
document.body.appendChild(e);
|
||
|
||
try {
|
||
// 设置 Canvas 位置
|
||
const x = 5 / o;
|
||
e.setCssStyles({
|
||
top: `${x}px`,
|
||
left: `${x}px`,
|
||
bottom: `${x}px`,
|
||
right: `${x}px`,
|
||
width: "auto",
|
||
height: "auto"
|
||
});
|
||
|
||
// 计算视口
|
||
const P = e.getBoundingClientRect();
|
||
const L = Math.ceil(P.x * o + 1);
|
||
const I = Math.ceil(P.y * o + 1);
|
||
const O = Math.floor(P.width * o) - 2;
|
||
const F = Math.floor(P.height * o) - 2;
|
||
|
||
// 设置缩放
|
||
// @ts-ignore
|
||
canvas.zoom = Math.log2(d / o);
|
||
// @ts-ignore
|
||
canvas.tZoom = canvas.zoom;
|
||
// @ts-ignore
|
||
canvas.viewportChanged = true;
|
||
canvas.requestFrame();
|
||
|
||
// 等待渲染
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||
|
||
// 计算分块数量
|
||
const tileWidth = O / d;
|
||
const tileHeight = F / d;
|
||
const cols = Math.ceil((bbox.maxX - bbox.minX) / tileWidth);
|
||
const rows = Math.ceil((bbox.maxY - bbox.minY) / tileHeight);
|
||
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");
|
||
finalCanvas.width = Math.ceil(c * d * s);
|
||
finalCanvas.height = Math.ceil(u * d * s);
|
||
const ctx = finalCanvas.getContext("2d");
|
||
if (!ctx) {
|
||
throw new Error("Failed to get 2d context");
|
||
}
|
||
|
||
let tileIndex = 0;
|
||
const totalTiles = cols * rows;
|
||
|
||
// 渲染每个分块
|
||
for (let row = 0; row < rows; row++) {
|
||
for (let col = 0; col < cols; col++) {
|
||
// 设置视口位置
|
||
// @ts-ignore
|
||
canvas.x = canvas.tx = startX + col * tileWidth;
|
||
// @ts-ignore
|
||
canvas.y = canvas.ty = startY + row * tileHeight;
|
||
// @ts-ignore
|
||
canvas.viewportChanged = true;
|
||
canvas.requestFrame();
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||
|
||
// 捕获当前视口
|
||
const capturedData = await webContents.capturePage({ x: L, y: I, width: O, height: F });
|
||
|
||
// 转换为图片并绘制到最终 Canvas
|
||
const pngData = capturedData.toPNG();
|
||
const blob = new Blob([pngData], { type: "image/png" });
|
||
const imageUrl = URL.createObjectURL(blob);
|
||
|
||
const img = document.createElement("img");
|
||
await new Promise<void>((resolve, reject) => {
|
||
img.onload = () => resolve();
|
||
img.onerror = reject;
|
||
img.src = imageUrl;
|
||
});
|
||
|
||
ctx.drawImage(img, col * O * s, row * F * s);
|
||
URL.revokeObjectURL(imageUrl);
|
||
|
||
tileIndex++;
|
||
console.debug(`Rendered tile ${tileIndex}/${totalTiles}`);
|
||
}
|
||
}
|
||
|
||
// 转换为 PNG
|
||
const pngBlob = await new Promise<Blob | null>((resolve) => finalCanvas.toBlob(resolve, "image/png"));
|
||
if (!pngBlob) {
|
||
throw new Error("Failed to create PNG blob");
|
||
}
|
||
const arrayBuffer = await pngBlob.arrayBuffer();
|
||
const uint8Array = new Uint8Array(arrayBuffer);
|
||
|
||
return uint8Array;
|
||
|
||
} finally {
|
||
// 清理
|
||
|
||
// @ts-ignore
|
||
this.app.workspace.getLeaf().view?.contentEl?.appendChild(e);
|
||
e.removeClass("is-screenshotting");
|
||
e.removeClass("is-text-garbled");
|
||
e.setCssStyles({
|
||
top: "",
|
||
left: "",
|
||
bottom: "",
|
||
right: "",
|
||
width: "",
|
||
height: ""
|
||
});
|
||
// @ts-ignore
|
||
canvas.screenshotting = false;
|
||
|
||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||
// @ts-ignore
|
||
canvas.zoomToFitQueued = true;
|
||
// @ts-ignore
|
||
canvas.onResize();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error("Canvas render failed:", error);
|
||
return null;
|
||
}
|
||
}
|
||
/* eslint-disable */
|
||
|
||
// 计算 Canvas 的边界框
|
||
calculateBBox(nodes: unknown[]): { minX: number, minY: number, maxX: number, maxY: number } {
|
||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||
|
||
for (const node of nodes) {
|
||
// @ts-ignore
|
||
const x = node.x;
|
||
// @ts-ignore
|
||
const y = node.y;
|
||
// @ts-ignore
|
||
const width = node.width;
|
||
// @ts-ignore
|
||
const height = node.height;
|
||
|
||
minX = Math.min(minX, x);
|
||
minY = Math.min(minY, y);
|
||
maxX = Math.max(maxX, x + width);
|
||
maxY = Math.max(maxY, y + height);
|
||
}
|
||
|
||
return { minX, minY, maxX, maxY };
|
||
}
|
||
|
||
// 取消选择所有节点
|
||
deselectAll(): void {
|
||
// @ts-ignore
|
||
const canvas = this.app.workspace.getLeaf().view?.canvas;
|
||
if (canvas) {
|
||
// @ts-ignore
|
||
canvas.deselectAll();
|
||
}
|
||
}
|
||
|
||
// 更新内容中的 Canvas 链接
|
||
updateCanvasLinksInContent(content: string, canvasFile: TFile, canvasPath: string, imageFileName: string): string {
|
||
// 获取源文件的目录路径
|
||
const sourceDir = canvasFile.path.substring(0, canvasFile.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', ''))
|
||
];
|
||
|
||
console.debug('Trying to replace canvas links for paths:', possiblePaths);
|
||
|
||
// 使用根路径: /image.png
|
||
const assetsRef = `/${imageFileName}`;
|
||
|
||
// 替换 markdown 格式: !(canvas) -> <Viewer src="/image.png" />
|
||
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}" />`;
|
||
}
|
||
}
|
||
return match;
|
||
});
|
||
|
||
// 替换 wikilink 格式: ![[canvas]] -> <Viewer src="/image.png" />
|
||
const wikilinkRegex = /!\[\[([^\]]+)\]\]/g;
|
||
content = content.replace(wikilinkRegex, (match, link) => {
|
||
// 移除可能的别名字段
|
||
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}" />`;
|
||
}
|
||
}
|
||
return match;
|
||
});
|
||
|
||
return content;
|
||
}
|
||
}
|