Skip to content

Instantly share code, notes, and snippets.

@curegit
Last active August 19, 2025 06:20
Show Gist options
  • Select an option

  • Save curegit/6bf83f4b225cd85da467d08531160981 to your computer and use it in GitHub Desktop.

Select an option

Save curegit/6bf83f4b225cd85da467d08531160981 to your computer and use it in GitHub Desktop.
画像と CSS を HTML に埋め込んでダウンロードするブックマークレット
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