fix: 预览时本地图片不显示

问题: 重构后丢失了原始版本的图片路径转换逻辑,
      相对路径图片(如 ![](./images/logo.png))无法显示。

修复:
- 新增 rehypeFixImages 自定义 rehype 插件
- 在 sanitize 之后、highlight 之前执行
- 将相对路径 src 转换为 file:// 绝对路径
- 基于当前文件路径计算目录,拼接图片路径
- renderMarkdown 新增 filePath 可选参数
- Preview 组件传入 activeTab.filePath

示例:
  Markdown: ![](./images/logo.png)
  文件路径: /project/docs/README.md
  转换后:   file:///project/docs/images/logo.png
This commit is contained in:
thzxx
2026-05-27 21:13:31 +08:00
parent f9aaf19e19
commit d23d209522
2 changed files with 50 additions and 16 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ export function Preview() {
} }
const requestId = ++requestIdRef.current const requestId = ++requestIdRef.current
renderMarkdown(activeTab.content).then(result => { renderMarkdown(activeTab.content, activeTab.filePath).then(result => {
// M-07: 只有当 requestId 仍为最新时才更新 // M-07: 只有当 requestId 仍为最新时才更新
if (requestId === requestIdRef.current) { if (requestId === requestIdRef.current) {
setHtml(result) setHtml(result)
+38 -4
View File
@@ -1,4 +1,4 @@
import { unified } from 'unified' import { unified, type Plugin } from 'unified'
import remarkParse from 'remark-parse' import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import remarkRehype from 'remark-rehype' import remarkRehype from 'remark-rehype'
@@ -6,8 +6,43 @@ import rehypeRaw from 'rehype-raw'
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize' import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify' import rehypeStringify from 'rehype-stringify'
import rehypeHighlight from 'rehype-highlight' import rehypeHighlight from 'rehype-highlight'
import type { Element, Root } from 'hast'
const processor = unified() // 自定义 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)
}
}
}
visit(tree)
}
}
export async function renderMarkdown(content: string, filePath?: string | null): Promise<string> {
try {
const processor = unified()
.use(remarkParse) .use(remarkParse)
.use(remarkGfm) .use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true }) .use(remarkRehype, { allowDangerousHtml: true })
@@ -19,11 +54,10 @@ const processor = unified()
img: [...(defaultSchema.attributes?.img ?? []), ['src']] img: [...(defaultSchema.attributes?.img ?? []), ['src']]
} }
}) })
.use(rehypeFixImages(filePath ?? null))
.use(rehypeHighlight) .use(rehypeHighlight)
.use(rehypeStringify) .use(rehypeStringify)
export async function renderMarkdown(content: string): Promise<string> {
try {
const result = await processor.process(content) const result = await processor.process(content)
return String(result) return String(result)
} catch (e) { } catch (e) {