import axios from "axios"; import fs from "node:fs"; import path from "node:path"; export interface AssetMap { [originalUrl: string]: string; } export class AssetManager { private userAgent: string; constructor( userAgent: string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", ) { this.userAgent = userAgent; } public sanitizePath(rawPath: string): string { return rawPath .split("/") .map((p) => p.replace(/[^a-z0-9._-]/gi, "_")) .join("/"); } public async downloadFile( url: string, assetsDir: string, ): Promise { if (url.startsWith("//")) url = `https:${url}`; if (!url.startsWith("http")) return null; try { const u = new URL(url); const relPath = this.sanitizePath(u.hostname + u.pathname); const dest = path.join(assetsDir, relPath); if (fs.existsSync(dest)) return `./assets/${relPath}`; const res = await axios.get(url, { responseType: "arraybuffer", headers: { "User-Agent": this.userAgent }, timeout: 15000, validateStatus: () => true, }); if (res.status !== 200) return null; if (!fs.existsSync(path.dirname(dest))) fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, Buffer.from(res.data)); return `./assets/${relPath}`; } catch { return null; } } public async processCssRecursively( cssContent: string, cssUrl: string, assetsDir: string, urlMap: AssetMap, depth = 0, ): Promise { if (depth > 5) return cssContent; const urlRegex = /(?:url\(["']?|@import\s+["'])([^"')]*)["']?\)?/gi; let match; let newContent = cssContent; while ((match = urlRegex.exec(cssContent)) !== null) { const originalUrl = match[1]; if (originalUrl.startsWith("data:") || originalUrl.startsWith("blob:")) continue; try { const absUrl = new URL(originalUrl, cssUrl).href; const local = await this.downloadFile(absUrl, assetsDir); if (local) { const u = new URL(cssUrl); const cssPath = u.hostname + u.pathname; const assetPath = new URL(absUrl).hostname + new URL(absUrl).pathname; const rel = path.relative( path.dirname(this.sanitizePath(cssPath)), this.sanitizePath(assetPath), ); newContent = newContent.split(originalUrl).join(rel); urlMap[absUrl] = local; } } catch { // Ignore } } return newContent; } }