fix(image): 编辑区相对路径图片显示解析(docs/16)

粘贴/插入的图片落盘与相对引用本身没问题,但编辑区从未解析相对路径:
页面在应用源下按相对路径取图必 404、只剩裂图。新增 image NodeView,
仅显示层把本地图片按文档目录归一化后经 read_file_base64 读成 data URL;
文档内容不变,Web 模式不启用。vitest 83 通过 + 2 跳过。
This commit is contained in:
Muse
2026-11-14 12:17:37 +08:00
parent 2ddd12082e
commit b6acbf6f30
6 changed files with 181 additions and 2 deletions

View File

@@ -0,0 +1,72 @@
// 图片显示解析回归:文档里的相对路径图片在编辑区经 NodeView 读成 data URL 显示;
// 文档内容(序列化)保持相对路径不变。upload 插件以 instanceof 把关事件类型,
// jsdom 缺 ClipboardEvent/DragEvent 全局,这里垫上。
import { describe, it, expect } from 'vitest'
import { createMarkdownEditor, getMarkdown } from '../src/editor/editor'
import { editorViewCtx } from '@milkdown/kit/core'
class FakeClipboardEvent extends Event {
clipboardData: unknown
constructor(type: string, init?: { clipboardData?: unknown }) {
super(type, { bubbles: true, cancelable: true })
this.clipboardData = init?.clipboardData
}
}
;(globalThis as Record<string, unknown>).ClipboardEvent = FakeClipboardEvent
;(globalThis as Record<string, unknown>).DragEvent = class extends Event {}
const tick = (ms = 40) => new Promise((r) => setTimeout(r, ms))
describe('图片显示解析', () => {
it('相对路径图片在 DOM 里被换成 data URL,文档仍是相对路径', async () => {
const host = document.createElement('div')
document.body.appendChild(host)
const editor = createMarkdownEditor(host, '看图 ![截图](doc.assets/a.png) 完', {}, {
resolveImageDisplay: async (src) => (src === 'doc.assets/a.png' ? 'data:image/png;base64,QUJD' : null),
})
await editor.create()
await tick()
const img = host.querySelector('img:not(.ProseMirror-separator)')
expect(img?.getAttribute('src')).toBe('data:image/png;base64,QUJD')
expect(img?.getAttribute('alt')).toBe('截图')
expect(getMarkdown(editor)).toContain('![截图](doc.assets/a.png)')
await editor.destroy()
host.remove()
})
it('粘贴图片落盘为相对路径后同样能显示', async () => {
const host = document.createElement('div')
document.body.appendChild(host)
const editor = createMarkdownEditor(host, '正文', {}, {
onImageFile: async () => 'doc.assets/pasted.png',
resolveImageDisplay: async (src) => (src === 'doc.assets/pasted.png' ? 'data:image/png;base64,QUJD' : null),
})
await editor.create()
const view = editor.action((ctx) => ctx.get(editorViewCtx))
const file = new File(['png'], 'image.png', { type: 'image/png' })
const event = new FakeClipboardEvent('paste', {
clipboardData: { files: [file], items: [], getData: () => '' },
})
view.dom.dispatchEvent(event)
await tick(80)
expect(getMarkdown(editor)).toContain('doc.assets/pasted.png')
const img = host.querySelector('img:not(.ProseMirror-separator)')
expect(img?.getAttribute('src')).toBe('data:image/png;base64,QUJD')
await editor.destroy()
host.remove()
})
it('解析不到(文件缺失)时不崩溃、文档不变', async () => {
const host = document.createElement('div')
document.body.appendChild(host)
const editor = createMarkdownEditor(host, '![x](missing.png)', {}, {
resolveImageDisplay: async () => null,
})
await editor.create()
await tick()
expect(host.querySelector('img')).not.toBeNull()
expect(getMarkdown(editor)).toContain('![x](missing.png)')
await editor.destroy()
host.remove()
})
})