Last active
February 25, 2025 13:25
-
-
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() { | |
async function stringifyRule(rule, contextStyleSheet) { | |
return rule instanceof CSSImportRule ? (rule.stylesheet ? await stringifyStyleSheet(rule.stylesheet) : "") : await inlinizeStyleImages(rule.cssText || "", (contextStyleSheet.href ?? contextStyleSheet.ownerNode?.baseURI ?? document.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 = document.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 = [...document.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 = document.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 = [...document.querySelectorAll("*[style]")]; | |
for (let element of elements) { | |
const style = element.getAttribute("style"); | |
const newStyle = await inlinizeStyleImages(style, element.baseURI ?? document.baseURI); | |
element.setAttribute("style", newStyle); | |
} | |
} | |
async function inlinizeStyleSheets() { | |
const promises = [...document.styleSheets].map(async x => { | |
const styleElement = document.createElement("style"); | |
styleElement.appendChild(document.createTextNode(await stringifyStyleSheet(x))); | |
x.ownerNode?.replaceWith?.(styleElement); | |
}); | |
await Promise.all(promises); | |
} | |
function removeScripts(all = true) { | |
const selector = all ? "script" : "script[src]"; | |
const scripts = document.querySelectorAll(selector); | |
scripts.forEach(script => script.remove()); | |
} | |
let del = () => null; | |
if (confirm("すべてのスクリプトを削除しますか?")) { | |
del = () => removeScripts(true); | |
} else if (confirm("外部スクリプトを削除しますか?")) { | |
del = () => removeScripts(false); | |
} | |
inlinizeImageElements(); | |
await inlinizeFavicon(); | |
await inlinizeStyleAttributes(); | |
await inlinizeStyleSheets(); | |
del(); | |
const originalURL = `<!-- Original URL: ${document.location.href} -->`; | |
const html = `<!DOCTYPE html>\n${originalURL}\n${document.body.parentNode.outerHTML}`; | |
const a = document.createElement("a"); | |
a.href = URL.createObjectURL(new Blob([html], { type: "text/html" })); | |
a.download = `${document.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