fix(markdown): preview local/relative images in .md files
The shared MarkdownText only renders absolute http(s) images (relative links are disabled for chat security), so a local image referenced by a previewed markdown file - , an absolute /cwd/img.png, or a reference definition - showed only its alt text. Rewrite such destinations into absolute /sidebar/file media URLs (with the GUI origin prefix) so MarkdownText accepts them and the host media route serves the bytes (still restricted to the session cwd). The rewriter is a dependency-free pure helper (markdown-images.ts) with a focused unit spec; TextEditor passes the rewritten preview text to both the plain MarkdownText and the mermaid-lazy preview.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { rewriteLocalImageUrls } from '../src/client/markdown-images.ts'
|
||||
import type { SessionScope } from '../src/client/api.ts'
|
||||
|
||||
const ORIGIN = 'http://127.0.0.1:3080'
|
||||
const scope: SessionScope = { sessionId: 'abc', cwd: '/repo' }
|
||||
|
||||
describe('rewriteLocalImageUrls', () => {
|
||||
it('resolves relative destinations against the opened file directory', () => {
|
||||
const md = '\n'
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/docs/readme.md', ORIGIN)
|
||||
expect(out).toContain(``)
|
||||
expect(out).toContain(``)
|
||||
})
|
||||
|
||||
it('expects the rewritten destination to be an absolute http URL MarkdownText accepts', () => {
|
||||
const out = rewriteLocalImageUrls('', scope, '/repo/readme.md', ORIGIN)
|
||||
const url = out.match(/\]\((http[^)]+)\)/)?.[1]
|
||||
expect(url).toBeDefined()
|
||||
expect(new URL(url as string).protocol).toBe('http:')
|
||||
})
|
||||
|
||||
it('passes absolute local paths through unchanged', () => {
|
||||
const md = ''
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/docs/readme.md', ORIGIN)
|
||||
expect(out).toContain(``)
|
||||
})
|
||||
|
||||
it('keeps remote http(s)/data/mailto destinations untouched', () => {
|
||||
const md = '\n\n'
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
|
||||
expect(out).toContain('')
|
||||
expect(out).toContain('')
|
||||
expect(out).toContain('')
|
||||
})
|
||||
|
||||
it('leaves in-page anchors untouched', () => {
|
||||
const md = '\n\n[link](#top)'
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
|
||||
expect(out).toContain('')
|
||||
})
|
||||
|
||||
it('rewrites reference-style image definitions', () => {
|
||||
const md = '![logo][logo]\n\n[logo]: ./logo.svg'
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
|
||||
expect(out).toContain(`[logo]: ${ORIGIN}/sidebar/file?sessionId=abc&path=%2Frepo%2Flogo.svg&cwd=%2Frepo`)
|
||||
// The inline reference itself is left for MarkdownText to combine.
|
||||
expect(out).toContain('![logo][logo]')
|
||||
})
|
||||
|
||||
it('normalizes `.`/`..` segments in relative destinations', () => {
|
||||
const md = '\n'
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/docs/sub/readme.md', ORIGIN)
|
||||
expect(out).toContain(``)
|
||||
expect(out).toContain(``)
|
||||
expect(out).not.toContain('/.')
|
||||
})
|
||||
|
||||
it('drops the cwd param when the scope has no cwd', () => {
|
||||
const out = rewriteLocalImageUrls('', { sessionId: 'abc' }, '/repo/readme.md', ORIGIN)
|
||||
expect(out).toContain(`sessionId=abc&path=%2Frepo%2Fimg.png`)
|
||||
expect(out).not.toContain('cwd=')
|
||||
})
|
||||
|
||||
it('handles windows absolute paths as local (not remote)', () => {
|
||||
const md = ''
|
||||
const out = rewriteLocalImageUrls(md, scope, 'C:\\repo\\docs\\readme.md', ORIGIN)
|
||||
// A Windows drive path is absolute (not a remote scheme) → rewritten.
|
||||
const url = out.match(/\]\(([^)]+)\)/)?.[1] as string
|
||||
expect(url).toContain('/sidebar/file?')
|
||||
expect(decodeURIComponent(url)).toContain('C:\\repo\\img.png')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user