90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
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<string, unknown> | 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<UploadedAsset>
|
|
): Promise<UploadedAsset | null> {
|
|
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<string, unknown> | 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<UploadedAsset>
|
|
): Promise<TipTapJSON> {
|
|
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
|
|
}
|