import type { TipTapJSON } from "@/lib/drive/richtext-import" const BASE64_PREFIX = "data:" /** Collect base64 image src values from TipTap JSON. */ export function collectBase64ImageSrcs(content: TipTapJSON): string[] { const srcs: string[] = [] const walk = (node: unknown) => { if (!node || typeof node !== "object") return const record = node as TipTapJSON const attrs = record.attrs as Record | undefined if ( (record.type === "image" || record.type === "docsGraphic" || record.type === "docsInlineGraphic") && typeof attrs?.src === "string" && attrs.src.startsWith(BASE64_PREFIX) ) { srcs.push(attrs.src) } if (Array.isArray(record.content)) record.content.forEach(walk) } walk(content) return srcs } export type UploadedAsset = { assetId: string url: string } /** Upload a base64 image and return asset reference. Phase 2 blob migration. */ export async function uploadGraphicAsset( src: string, uploadFn: (body: { dataUrl: string }) => Promise ): Promise { if (!src.startsWith(BASE64_PREFIX)) return null try { return await uploadFn({ dataUrl: src }) } catch { return null } } /** Replace base64 src with asset URL in TipTap JSON (lazy migration). */ export function applyAssetToContent( content: TipTapJSON, src: string, asset: UploadedAsset ): TipTapJSON { const walk = (node: unknown): unknown => { if (!node || typeof node !== "object") return node if (Array.isArray(node)) return node.map(walk) const record = node as TipTapJSON const attrs = record.attrs as Record | undefined if ( attrs && attrs.src === src && (record.type === "image" || record.type === "docsGraphic" || record.type === "docsInlineGraphic") ) { return { ...record, attrs: { ...attrs, src: asset.url, assetId: asset.assetId }, } } if (Array.isArray(record.content)) { return { ...record, content: record.content.map(walk) } } return record } const next = walk(content) return (next && typeof next === "object" ? next : content) as TipTapJSON } /** Migrate all base64 images in document to backend assets when uploadFn provided. */ export async function migrateBase64ImagesInContent( content: TipTapJSON, uploadFn: (body: { dataUrl: string }) => Promise ): Promise { let result = content const srcs = [...new Set(collectBase64ImageSrcs(content))] for (const src of srcs) { const asset = await uploadGraphicAsset(src, uploadFn) if (asset) result = applyAssetToContent(result, src, asset) } return result }