(async () => { const rootHandle = await window.showDirectoryPicker(); const origin = location.origin; const currentUrl = new URL(location.href); const textDecoder = new TextDecoder(); const normalizeUrl = (u) => { try { const x = new URL(u, location.href); if (!/^https?:$/.test(x.protocol)) return null; if (x.origin !== origin) return null; // same-origin only x.hash = ""; return x; } catch { return null; } }; const toLocalPathParts = (urlObj, isHtml = false) => { let path = decodeURIComponent(urlObj.pathname); if (!path || path === "/") { return ["index.html"]; } const parts = path.split("/").filter(Boolean); const last = parts[parts.length - 1] || ""; const hasExt = /\.[a-zA-Z0-9]{1,10}$/.test(last); if (isHtml) { if (hasExt && /\.html?$/i.test(last)) { return parts; } if (hasExt) { return parts; } return [...parts, "index.html"]; } if (!hasExt) { return [...parts, "index.html"]; } return parts; }; const getOrCreateDir = async (baseHandle, dirParts) => { let h = baseHandle; for (const part of dirParts) { h = await h.getDirectoryHandle(part, { create: true }); } return h; }; const writeFile = async (baseHandle, pathParts, blob) => { const dirs = pathParts.slice(0, -1); const fileName = pathParts[pathParts.length - 1]; const dirHandle = await getOrCreateDir(baseHandle, dirs); const fileHandle = await dirHandle.getFileHandle(fileName, { create: true }); const writable = await fileHandle.createWritable(); await writable.write(blob); await writable.close(); }; const collected = new Map(); const addUrl = (u, type = "asset") => { const nu = normalizeUrl(u); if (!nu) return; const key = nu.href; if (!collected.has(key)) { collected.set(key, { url: nu, type }); } }; // current page addUrl(location.href, "html"); // DOM resources document.querySelectorAll("script[src]").forEach(el => addUrl(el.src, "asset")); document.querySelectorAll('link[href]').forEach(el => addUrl(el.href, "asset")); document.querySelectorAll("img[src]").forEach(el => addUrl(el.src, "asset")); document.querySelectorAll("[style]").forEach(el => { const style = el.getAttribute("style") || ""; const re = /url\((['"]?)(.*?)\1\)/g; let m; while ((m = re.exec(style))) addUrl(m[2], "asset"); }); // CSS rules for (const sheet of Array.from(document.styleSheets)) { try { for (const rule of Array.from(sheet.cssRules || [])) { const txt = rule.cssText || ""; const re = /url\((['"]?)(.*?)\1\)/g; let m; while ((m = re.exec(txt))) addUrl(m[2], "asset"); } } catch (e) { // cross-origin stylesheet access can fail } } // Performance resources performance.getEntriesByType("resource").forEach(r => addUrl(r.name, "asset")); // Save HTML of current page into path-based index.html const currentHtmlParts = toLocalPathParts(currentUrl, true); const htmlBlob = new Blob([document.documentElement.outerHTML], { type: "text/html;charset=utf-8" }); await writeFile(rootHandle, currentHtmlParts, htmlBlob); let ok = 0; let fail = 0; const failed = []; for (const { url, type } of collected.values()) { if (type === "html" && url.href === currentUrl.href) continue; try { const res = await fetch(url.href, { credentials: "include" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); const pathParts = toLocalPathParts(url, false); await writeFile(rootHandle, pathParts, blob); ok++; console.log("saved:", url.href, "->", pathParts.join("/")); } catch (e) { fail++; failed.push({ url: url.href, error: String(e) }); console.warn("failed:", url.href, e); } } // save fail report const report = { page: location.href, savedHtmlAs: currentHtmlParts.join("/"), savedCount: ok, failedCount: fail, failed }; await writeFile( rootHandle, ["_download_report.json"], new Blob([JSON.stringify(report, null, 2)], { type: "application/json" }) ); console.log("DONE"); console.log("HTML:", currentHtmlParts.join("/")); console.log("saved assets:", ok); console.log("failed assets:", fail); })();