// Shared by the server and canvas; no storage or network work happens here. export type ArchivedImageUrlMap = Record; export function isGeneratedImageSource(value: string) { return ( /^https?:\/\//i.test(value) || /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=]+$/i.test(value) ); } export function collectImageArchiveSources(value: unknown): string[] { const urls = new Set(); const visit = (item: unknown) => { if (typeof item === "string") { if (isGeneratedImageSource(item)) urls.add(item); else for (const match of item.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)) { if (isGeneratedImageSource(match[1])) urls.add(match[1]); } } else if (Array.isArray(item)) item.forEach(visit); else if (item && typeof item === "object" && !(item instanceof Date)) Object.values(item).forEach(visit); }; visit(value); return [...urls]; } // Structural sharing preserves unaffected nodes and Date values; no undo step is added. export function replaceArchivedImageUrls(value: T, replacements: ArchivedImageUrlMap): T { if (typeof value === "string") { if (replacements[value]) return replacements[value] as T; // URLs inside generated-image Markdown carry both preview and original URLs. return value.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt: string, url: string) => { const nextAlt = alt .split("|||") .map((part) => replacements[part] || part) .join("|||"); return `![${nextAlt}](${replacements[url] || url})`; }) as T; } if (!value || typeof value !== "object" || value instanceof Date) return value; let changed = false; if (Array.isArray(value)) { const next = value.map((item) => { const replaced = replaceArchivedImageUrls(item, replacements); if (replaced !== item) changed = true; return replaced; }); return (changed ? next : value) as T; } const next: Record = {}; for (const [key, item] of Object.entries(value)) { next[key] = replaceArchivedImageUrls(item, replacements); if (next[key] !== item) changed = true; } return (changed ? next : value) as T; }