publish(task 5): markdown bild-url-rewriter (mapping-basiert, =WxH-strip)

rewriteImageUrls(md, mapping) ersetzt alle ![alt](filename)- und
[![alt](filename)](link)-konstrukte per dateinamen-lookup durch
blossom-urls aus dem mapping. absolute urls bleiben unverändert,
unbekannte dateinamen bleiben stehen (kein strip). =WxH-größenhinweise
werden entfernt. URL-dekodierung für leerzeichen-dateinamen (z.b.
'03-config generieren.png' im moodle-post). resolveCoverUrl()-helper
für den cover-lookup. 7 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-18 05:24:03 +02:00
parent bc2679aeba
commit a6c5bd26e7
2 changed files with 82 additions and 0 deletions

View File

@ -0,0 +1,29 @@
const IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+=\d+x\d+)?\)/g
function isAbsolute(url: string): boolean {
return /^(https?:)?\/\//i.test(url)
}
export function rewriteImageUrls(md: string, mapping: Map<string, string>): string {
return md.replace(IMG_RE, (full, alt: string, url: string) => {
if (isAbsolute(url)) return full.replace(/\s+=\d+x\d+\)$/, ')')
let decoded: string
try {
decoded = decodeURIComponent(url)
} catch {
decoded = url
}
const target = mapping.get(decoded) ?? mapping.get(url)
if (!target) return full.replace(/\s+=\d+x\d+\)$/, ')')
return `![${alt}](${target})`
})
}
export function resolveCoverUrl(
coverRaw: string | undefined,
mapping: Map<string, string>,
): string | undefined {
if (!coverRaw) return undefined
if (isAbsolute(coverRaw)) return coverRaw
return mapping.get(coverRaw)
}

View File

@ -0,0 +1,53 @@
import { assertEquals } from '@std/assert'
import { rewriteImageUrls } from '../src/core/markdown.ts'
Deno.test('rewriteImageUrls: ersetzt ![alt](file) durch Mapping', () => {
const mapping = new Map([['cat.png', 'https://blossom.example/hash.png']])
const input = '![cat](cat.png)'
assertEquals(rewriteImageUrls(input, mapping), '![cat](https://blossom.example/hash.png)')
})
Deno.test('rewriteImageUrls: absolute URL bleibt unverändert', () => {
const mapping = new Map([['cat.png', 'https://blossom.example/hash.png']])
const input = '![cat](https://other.com/cat.png)'
assertEquals(rewriteImageUrls(input, mapping), input)
})
Deno.test('rewriteImageUrls: entfernt =WxH-Suffix', () => {
const mapping = new Map([['cat.png', 'https://blossom.example/hash.png']])
const input = '![cat](cat.png =300x200)'
assertEquals(rewriteImageUrls(input, mapping), '![cat](https://blossom.example/hash.png)')
})
Deno.test('rewriteImageUrls: bild-in-link [![alt](file)](link)', () => {
const mapping = new Map([['cat.png', 'https://blossom.example/hash.png']])
const input = '[![cat](cat.png)](https://target.example.com)'
assertEquals(
rewriteImageUrls(input, mapping),
'[![cat](https://blossom.example/hash.png)](https://target.example.com)',
)
})
Deno.test('rewriteImageUrls: mehrere Bilder im Text', () => {
const mapping = new Map([
['a.png', 'https://bl/a-hash.png'],
['b.jpg', 'https://bl/b-hash.jpg'],
])
const input = 'Text ![a](a.png) more ![b](b.jpg) end'
assertEquals(
rewriteImageUrls(input, mapping),
'Text ![a](https://bl/a-hash.png) more ![b](https://bl/b-hash.jpg) end',
)
})
Deno.test('rewriteImageUrls: lässt unbekannte Dateinamen stehen', () => {
const mapping = new Map([['cat.png', 'https://bl/c.png']])
const input = '![x](missing.jpg)'
assertEquals(rewriteImageUrls(input, mapping), input)
})
Deno.test('rewriteImageUrls: URL-Dekodierung für Leerzeichen-Namen', () => {
const mapping = new Map([['file with spaces.png', 'https://bl/hash.png']])
const input = '![x](file%20with%20spaces.png)'
assertEquals(rewriteImageUrls(input, mapping), '![x](https://bl/hash.png)')
})