diff --git a/src/renderer/components/Preview/Preview.tsx b/src/renderer/components/Preview/Preview.tsx index d637406..70ebd20 100644 --- a/src/renderer/components/Preview/Preview.tsx +++ b/src/renderer/components/Preview/Preview.tsx @@ -19,7 +19,7 @@ export function Preview() { } const requestId = ++requestIdRef.current - renderMarkdown(activeTab.content).then(result => { + renderMarkdown(activeTab.content, activeTab.filePath).then(result => { // M-07: 只有当 requestId 仍为最新时才更新 if (requestId === requestIdRef.current) { setHtml(result) diff --git a/src/renderer/lib/markdown.ts b/src/renderer/lib/markdown.ts index 9c584b5..f80ec09 100644 --- a/src/renderer/lib/markdown.ts +++ b/src/renderer/lib/markdown.ts @@ -1,4 +1,4 @@ -import { unified } from 'unified' +import { unified, type Plugin } from 'unified' import remarkParse from 'remark-parse' import remarkGfm from 'remark-gfm' import remarkRehype from 'remark-rehype' @@ -6,24 +6,58 @@ import rehypeRaw from 'rehype-raw' import rehypeSanitize, { defaultSchema } from 'rehype-sanitize' import rehypeStringify from 'rehype-stringify' import rehypeHighlight from 'rehype-highlight' +import type { Element, Root } from 'hast' -const processor = unified() - .use(remarkParse) - .use(remarkGfm) - .use(remarkRehype, { allowDangerousHtml: true }) - .use(rehypeRaw) - .use(rehypeSanitize, { - ...defaultSchema, - attributes: { - ...defaultSchema.attributes, - img: [...(defaultSchema.attributes?.img ?? []), ['src']] +// 自定义 rehype 插件:将相对路径图片转为 file:// 绝对路径 +function rehypeFixImages(filePath: string | null): Plugin<[], Root> { + return () => (tree: Root) => { + if (!filePath) return + + // 获取文件所在目录 + const dir = filePath.replace(/[/\\][^/\\]+$/, '') + const sep = dir.includes('\\') ? '\\' : '/' + + function visit(node: Element | Root): void { + if (!node.children) return + for (const child of node.children) { + if (child.type === 'element' && child.tagName === 'img') { + const src = child.properties?.src as string | undefined + if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:') && !src.startsWith('file://')) { + // 相对路径转绝对路径 + child.properties = { + ...child.properties, + src: 'file://' + (dir + sep + src).replace(/\\/g, '/') + } + } + } + if (child.type === 'element') { + visit(child as Element) + } + } } - }) - .use(rehypeHighlight) - .use(rehypeStringify) -export async function renderMarkdown(content: string): Promise { + visit(tree) + } +} + +export async function renderMarkdown(content: string, filePath?: string | null): Promise { try { + const processor = unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeSanitize, { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + img: [...(defaultSchema.attributes?.img ?? []), ['src']] + } + }) + .use(rehypeFixImages(filePath ?? null)) + .use(rehypeHighlight) + .use(rehypeStringify) + const result = await processor.process(content) return String(result) } catch (e) {