2 changed files with 105 additions and 0 deletions
@ -0,0 +1,57 @@ |
|||||
|
import { describe, expect, test } from "bun:test"; |
||||
|
import { |
||||
|
buildMarkdownExportFileName, |
||||
|
normalizeMarkdownImageUrls, |
||||
|
} from "./markdown-export"; |
||||
|
|
||||
|
describe("normalizeMarkdownImageUrls", () => { |
||||
|
test("converts /public/assets image links to absolute URLs", () => { |
||||
|
const markdown = ""; |
||||
|
const result = normalizeMarkdownImageUrls(markdown, "https://example.com"); |
||||
|
|
||||
|
expect(result).toBe(""); |
||||
|
}); |
||||
|
|
||||
|
test("keeps absolute http/https image links unchanged", () => { |
||||
|
const markdown = [ |
||||
|
"", |
||||
|
"", |
||||
|
].join("\n"); |
||||
|
|
||||
|
const result = normalizeMarkdownImageUrls(markdown, "https://example.com"); |
||||
|
|
||||
|
expect(result).toBe(markdown); |
||||
|
}); |
||||
|
|
||||
|
test("keeps protocol-relative and data URI image links unchanged", () => { |
||||
|
const markdown = [ |
||||
|
"", |
||||
|
"", |
||||
|
].join("\n"); |
||||
|
|
||||
|
const result = normalizeMarkdownImageUrls(markdown, "https://example.com"); |
||||
|
|
||||
|
expect(result).toBe(markdown); |
||||
|
}); |
||||
|
|
||||
|
test("does not change normal markdown links", () => { |
||||
|
const markdown = "[read more](/public/assets/posts/cover.png)"; |
||||
|
const result = normalizeMarkdownImageUrls(markdown, "https://example.com"); |
||||
|
|
||||
|
expect(result).toBe(markdown); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("buildMarkdownExportFileName", () => { |
||||
|
test("prefers slug when slug is not empty", () => { |
||||
|
const result = buildMarkdownExportFileName({ slug: "hello-world", id: 42 }); |
||||
|
|
||||
|
expect(result).toBe("hello-world.md"); |
||||
|
}); |
||||
|
|
||||
|
test("falls back to post-<id>.md when slug is empty", () => { |
||||
|
const result = buildMarkdownExportFileName({ slug: "", id: 42 }); |
||||
|
|
||||
|
expect(result).toBe("post-42.md"); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,48 @@ |
|||||
|
type MarkdownExportFileNameInput = { |
||||
|
slug: string; |
||||
|
id: number; |
||||
|
}; |
||||
|
|
||||
|
const PUBLIC_ASSETS_PREFIX = "/public/assets/"; |
||||
|
|
||||
|
function isAbsoluteOrSpecialUrl(url: string): boolean { |
||||
|
return /^(https?:)?\/\//i.test(url) || /^data:/i.test(url); |
||||
|
} |
||||
|
|
||||
|
export function normalizeMarkdownImageUrls(markdown: string, origin: string): string { |
||||
|
const normalizedOrigin = origin.replace(/\/+$/, ""); |
||||
|
|
||||
|
return markdown.replace(/!\[([^\]]*)\]\(([^)\s]+)([^)]*)\)/g, (full, alt, rawUrl, rest) => { |
||||
|
if (isAbsoluteOrSpecialUrl(rawUrl)) { |
||||
|
return full; |
||||
|
} |
||||
|
|
||||
|
if (!rawUrl.startsWith(PUBLIC_ASSETS_PREFIX)) { |
||||
|
return full; |
||||
|
} |
||||
|
|
||||
|
return ``; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function buildMarkdownExportFileName(input: MarkdownExportFileNameInput): string { |
||||
|
const base = input.slug.trim() || `post-${input.id}`; |
||||
|
return `${base}.md`; |
||||
|
} |
||||
|
|
||||
|
export function downloadMarkdownFile(filename: string, content: string): void { |
||||
|
if (typeof window === "undefined" || typeof document === "undefined") { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
const anchor = document.createElement("a"); |
||||
|
anchor.href = url; |
||||
|
anchor.download = filename; |
||||
|
anchor.style.display = "none"; |
||||
|
document.body.appendChild(anchor); |
||||
|
anchor.click(); |
||||
|
anchor.remove(); |
||||
|
URL.revokeObjectURL(url); |
||||
|
} |
||||
Loading…
Reference in new issue