Last active
August 19, 2025 06:20
-
-
Save curegit/6bf83f4b225cd85da467d08531160981 to your computer and use it in GitHub Desktop.
画像と CSS を HTML に埋め込んでダウンロードするブックマークレット
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javascript: (async function (doc_root = window.document) { | |
| async function embed(doc_root, inlinizeIframes = true, removeScripts = true, removeExternalScripts = true) { | |
| async function stringifyRule(rule, contextStyleSheet) { | |
| return rule instanceof CSSImportRule ? (rule.stylesheet ? await stringifyStyleSheet(rule.stylesheet) : "") : await inlinizeStyleImages(rule.cssText || "", contextStyleSheet.href ?? contextStyleSheet.ownerNode?.baseURI ?? doc_root.baseURI); | |
| } | |
| async function stringifyStyleSheet(style) { | |
| try { | |
| return style.cssRules ? (await Promise.all([...style.cssRules].map((r) => stringifyRule(r, style)))).join("\n") : ""; | |
| } catch (e) { | |
| console.warn(e); | |
| return ""; | |
| } | |
| } | |
| function imageToDataURL(image) { | |
| const canvas = doc_root.createElement("canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| try { | |
| canvas.width = image.naturalWidth ? image.naturalWidth : image.width; | |
| canvas.height = image.naturalHeight ? image.naturalHeight : image.height; | |
| ctx.drawImage(image, 0, 0, canvas.width, canvas.height); | |
| return canvas.toDataURL(); | |
| } catch (e) { | |
| console.warn(e); | |
| return false; | |
| } | |
| } | |
| async function inlinizeStyleImages(ruleText, basePath) { | |
| /* The URL extraction here is not perfect, but it is left as is for simplicity */ | |
| const url = ruleText.match(/url\(\s*?['"]?\s*?(.+?)\s*?["']?\s*?\)/i)?.[1]; | |
| if (!url || url.startsWith("data:")) return ruleText; | |
| const image = new Image(); | |
| const loadPromise = new Promise((r) => image.addEventListener("load", () => r())); | |
| const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1000)); | |
| image.src = "" + new URL(url, basePath); | |
| try { | |
| await Promise.race([loadPromise, timeoutPromise]); | |
| const data = imageToDataURL(image); | |
| if (data) { | |
| return ruleText.replace(url, data); | |
| } else { | |
| return ruleText; | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| return ruleText; | |
| } | |
| } | |
| function inlinizeImageElements() { | |
| const images = [...doc_root.images]; | |
| for (let i = 0; i < images.length; i++) { | |
| const image = images[i]; | |
| if (!image.src?.startsWith("data:")) { | |
| const data = imageToDataURL(image); | |
| if (data) { | |
| image.src = data; | |
| image.removeAttribute("srcset"); | |
| } | |
| } | |
| } | |
| } | |
| async function inlinizeFavicon() { | |
| const link = doc_root.querySelector("link[rel~='icon']"); | |
| if (link) { | |
| const image = new Image(); | |
| const loadPromise = new Promise((r) => image.addEventListener("load", () => r())); | |
| const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1000)); | |
| image.src = link.href; | |
| try { | |
| await Promise.race([loadPromise, timeoutPromise]); | |
| const data = imageToDataURL(image); | |
| if (data) { | |
| link.href = data; | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| } | |
| } | |
| } | |
| async function inlinizeStyleAttributes() { | |
| const elements = [...doc_root.querySelectorAll("*[style]")]; | |
| for (let element of elements) { | |
| const style = element.getAttribute("style"); | |
| const newStyle = await inlinizeStyleImages(style, element.baseURI ?? doc_root.baseURI); | |
| element.setAttribute("style", newStyle); | |
| } | |
| } | |
| async function inlinizeStyleSheets() { | |
| const promises = [...doc_root.styleSheets].map(async (x) => { | |
| const styleElement = doc_root.createElement("style"); | |
| styleElement.appendChild(doc_root.createTextNode(await stringifyStyleSheet(x))); | |
| x.ownerNode?.replaceWith?.(styleElement); | |
| }); | |
| await Promise.all(promises); | |
| } | |
| function blobToDataURL(blob) { | |
| return new Promise((resolve, reject) => { | |
| const fileReader = new FileReader(); | |
| fileReader.addEventListener("load", () => resolve(fileReader.result)); | |
| fileReader.addEventListener("error", () => reject(fileReader.error)); | |
| fileReader.readAsDataURL(blob); | |
| }); | |
| } | |
| async function processIframes() { | |
| const iframes = [...doc_root.querySelectorAll("iframe")]; | |
| for (let iframe of iframes) { | |
| let src_success = false; | |
| try { | |
| if (iframe.src) { | |
| const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; | |
| if (iframeDoc) { | |
| const { doctype, originalURL, html } = await embed(iframeDoc, inlinizeIframes, removeScripts, removeExternalScripts); | |
| iframe.src = await blobToDataURL(new Blob([doctype, originalURL, html], { type: "text/html" })); | |
| src_success = true; | |
| } | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| } | |
| try { | |
| if (iframe.srcdoc) { | |
| const iframeDoc = new DOMParser().parseFromString(iframe.srcdoc, "text/html"); | |
| const { doctype, originalURL, html } = await embed(iframeDoc, inlinizeIframes, removeScripts, removeExternalScripts); | |
| iframe.srcdoc = html; | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| if (src_success) { | |
| iframe.removeAttribute("srcdoc"); | |
| } | |
| } | |
| } | |
| } | |
| function removeScripts(all = true) { | |
| const selector = all ? "script" : "script[src]"; | |
| const scripts = doc_root.querySelectorAll(selector); | |
| scripts.forEach((script) => script.remove()); | |
| } | |
| inlinizeImageElements(); | |
| await inlinizeFavicon(); | |
| await inlinizeStyleAttributes(); | |
| await inlinizeStyleSheets(); | |
| if (inlinizeIframes) { | |
| await processIframes(); | |
| } | |
| if (removeScripts) { | |
| removeScripts(true); | |
| } else if (removeExternalScripts) { | |
| removeScripts(false); | |
| } | |
| const doctype = "<!DOCTYPE html>\n"; | |
| const originalURL = `<!-- Original URL: ${doc_root.location.href} -->\n`; | |
| const html = `${doc_root.body.parentNode.outerHTML}\n`; | |
| return { doctype, originalURL, html }; | |
| } | |
| let removeScripts = false; | |
| let removeExternalScripts = false; | |
| if (confirm("すべてのスクリプトを削除しますか?")) { | |
| removeScripts = true; | |
| removeExternalScripts = true; | |
| } else if (confirm("外部スクリプトを削除しますか?")) { | |
| removeExternalScripts = true; | |
| } | |
| const { doctype, originalURL, html } = await embed(doc_root, true, removeScripts, removeExternalScripts); | |
| const a = window.document.createElement("a"); | |
| a.href = URL.createObjectURL(new Blob([doctype, originalURL, html], { type: "text/html" })); | |
| a.download = `${doc_root.title}.html`; | |
| a.dispatchEvent(new MouseEvent("click")); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment