feat: image detection, naming strategies, body rewrite
Co-Authored-By: Claude
This commit is contained in:
106
src/images.ts
Normal file
106
src/images.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
export type Strategy = "flat-slug" | "per-post-folder";
|
||||
|
||||
export interface ImageRef {
|
||||
raw: string;
|
||||
linktext: string;
|
||||
alt: string;
|
||||
kind: "embed" | "markdown" | "html";
|
||||
}
|
||||
|
||||
export interface ImagePlanItem {
|
||||
linktext: string;
|
||||
repoPath: string;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
export interface PlanResult {
|
||||
rewrittenBody: string;
|
||||
plan: ImagePlanItem[];
|
||||
}
|
||||
|
||||
const IMG_EXT = /\.(png|jpe?g|gif|webp|svg|avif|bmp|tiff?)$/i;
|
||||
|
||||
function isLocal(path: string): boolean {
|
||||
return !/^[a-z]+:\/\//i.test(path) && !path.startsWith("/") && IMG_EXT.test(path);
|
||||
}
|
||||
|
||||
export function findImageRefs(body: string): ImageRef[] {
|
||||
const refs: ImageRef[] = [];
|
||||
|
||||
// Obsidian embed syntax: ![[linktext]] or ![[linktext|alt]]
|
||||
const embed = /!\[\[([^\]|#^]+?)(?:#[^\]|]*)?(?:\|([^\]]*))?\]\]/g;
|
||||
for (let m; (m = embed.exec(body)); ) {
|
||||
if (!isLocal(m[1].trim())) continue;
|
||||
refs.push({ raw: m[0], linktext: m[1].trim(), alt: (m[2] ?? "").trim(), kind: "embed" });
|
||||
}
|
||||
|
||||
// Markdown image: 
|
||||
const md = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
||||
for (let m; (m = md.exec(body)); ) {
|
||||
if (!isLocal(m[2])) continue;
|
||||
refs.push({ raw: m[0], linktext: m[2], alt: m[1], kind: "markdown" });
|
||||
}
|
||||
|
||||
// HTML img: <img src="..." alt="...">
|
||||
const html = /<img\b[^>]*?\bsrc=["']([^"']+)["'][^>]*?>/gi;
|
||||
for (let m; (m = html.exec(body)); ) {
|
||||
if (!isLocal(m[1])) continue;
|
||||
const altM = /\balt=["']([^"']*)["']/i.exec(m[0]);
|
||||
refs.push({ raw: m[0], linktext: m[1], alt: altM ? altM[1] : "", kind: "html" });
|
||||
}
|
||||
|
||||
// Order refs by position of their raw match for deterministic numbering.
|
||||
return refs.sort((a, b) => body.indexOf(a.raw) - body.indexOf(b.raw));
|
||||
}
|
||||
|
||||
function ext(path: string): string {
|
||||
const m = IMG_EXT.exec(path);
|
||||
return m ? m[0] : "";
|
||||
}
|
||||
|
||||
function basename(path: string): string {
|
||||
const parts = path.split("/");
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
export function rewriteBody(body: string, refs: ImageRef[], plan: ImagePlanItem[]): string {
|
||||
const url = new Map(plan.map((p) => [p.linktext, p.siteUrl]));
|
||||
let out = body;
|
||||
for (const ref of refs) {
|
||||
const siteUrl = url.get(ref.linktext);
|
||||
if (!siteUrl) continue;
|
||||
out = out.replace(ref.raw, ``);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function planImages(
|
||||
refs: ImageRef[],
|
||||
opts: { slug: string; strategy: Strategy; imagesDir: string; body: string }
|
||||
): PlanResult {
|
||||
const { slug, strategy, imagesDir, body } = opts;
|
||||
const byLinktext = new Map<string, ImagePlanItem>();
|
||||
const usedNames = new Set<string>();
|
||||
const distinct = refs.filter((r, i) => refs.findIndex((o) => o.linktext === r.linktext) === i);
|
||||
|
||||
distinct.forEach((ref, i) => {
|
||||
let repoRel: string;
|
||||
if (strategy === "flat-slug") {
|
||||
const suffix = distinct.length > 1 ? `-${i + 1}` : "";
|
||||
repoRel = `${slug}${suffix}${ext(ref.linktext)}`;
|
||||
} else {
|
||||
let name = basename(ref.linktext);
|
||||
while (usedNames.has(`${slug}/${name}`)) {
|
||||
const e = ext(name);
|
||||
name = `${name.slice(0, name.length - e.length)}-1${e}`;
|
||||
}
|
||||
usedNames.add(`${slug}/${name}`);
|
||||
repoRel = `${slug}/${name}`;
|
||||
}
|
||||
const repoPath = `${imagesDir}/${repoRel}`;
|
||||
byLinktext.set(ref.linktext, { linktext: ref.linktext, repoPath, siteUrl: `/${repoPath}` });
|
||||
});
|
||||
|
||||
const plan = distinct.map((r) => byLinktext.get(r.linktext)!);
|
||||
return { plan, rewrittenBody: rewriteBody(body, refs, plan) };
|
||||
}
|
||||
Reference in New Issue
Block a user