super_inline_html.js
1 #!/usr/bin/env node 2 /** 3 * super_inline_html.js — Bundle HTML + all linked assets into a single self-contained file 4 * 5 * Inlines: <link> CSS, <script src> JS, <img src> as data URLs 6 * 7 * Usage: 8 * node super_inline_html.js --input page.html --output page-inline.html 9 */ 10 11 const fs = require('fs'); 12 const path = require('path'); 13 const parseArgs = require('./lib/parse_args'); 14 15 function fileToDataUrl(filePath, mimeType) { 16 const data = fs.readFileSync(filePath); 17 return `data:${mimeType};base64,${data.toString('base64')}`; 18 } 19 20 function getMimeType(ext) { 21 const types = { 22 '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', 23 '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', 24 '.ico': 'image/x-icon', '.css': 'text/css', '.js': 'text/javascript', 25 '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', 26 }; 27 return types[ext.toLowerCase()] || 'application/octet-stream'; 28 } 29 30 function inlineHtml(html, baseDir) { 31 let result = html; 32 33 // Inline <link rel="stylesheet" href="..."> 34 result = result.replace(/<link\s+[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*\/?>/gi, (match, href) => { 35 if (href.startsWith('http') || href.startsWith('data:')) return match; 36 const filePath = path.resolve(baseDir, href); 37 if (!fs.existsSync(filePath)) return match; 38 const css = fs.readFileSync(filePath, 'utf-8'); 39 return `<style>\n${css}\n</style>`; 40 }); 41 42 // Inline <script src="..."> 43 result = result.replace(/<script\s+[^>]*src=["']([^"']+)["'][^>]*><\/script>/gi, (match, href) => { 44 if (href.startsWith('http') || href.startsWith('data:')) return match; 45 // Skip external CDN scripts 46 if (href.includes('unpkg.com') || href.includes('cdn.')) return match; 47 const filePath = path.resolve(baseDir, href); 48 if (!fs.existsSync(filePath)) return match; 49 const js = fs.readFileSync(filePath, 'utf-8'); 50 return `<script>\n${js}\n</script>`; 51 }); 52 53 // Inline <img src="..."> 54 result = result.replace(/<img\s+[^>]*src=["']([^"']+)["']/gi, (match, src) => { 55 if (src.startsWith('http') || src.startsWith('data:')) return match; 56 const filePath = path.resolve(baseDir, src); 57 if (!fs.existsSync(filePath)) return match; 58 const ext = path.extname(filePath); 59 const dataUrl = fileToDataUrl(filePath, getMimeType(ext)); 60 return match.replace(src, dataUrl); 61 }); 62 63 // Inline url() only within <style> blocks and style="" attributes (avoid matching JS strings) 64 // Process <style> blocks first 65 result = result.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match, cssContent) => { 66 const inlined = cssContent.replace(/url\(["']?([^"')]+)["']?\)/gi, (urlMatch, urlPath) => { 67 if (urlPath.startsWith('http') || urlPath.startsWith('data:')) return urlMatch; 68 const filePath = path.resolve(baseDir, urlPath); 69 if (!fs.existsSync(filePath)) return urlMatch; 70 const ext = path.extname(filePath); 71 const dataUrl = fileToDataUrl(filePath, getMimeType(ext)); 72 return `url("${dataUrl}")`; 73 }); 74 return `<style>${inlined}</style>`; 75 }); 76 77 // Process style="" attributes 78 result = result.replace(/style=["']([^"']*url\([^"']*?)[^"']*["']/gi, (match) => { 79 return match.replace(/url\(["']?([^"')]+)["']?\)/gi, (urlMatch, urlPath) => { 80 if (urlPath.startsWith('http') || urlPath.startsWith('data:')) return urlMatch; 81 const filePath = path.resolve(baseDir, urlPath); 82 if (!fs.existsSync(filePath)) return urlMatch; 83 const ext = path.extname(filePath); 84 const dataUrl = fileToDataUrl(filePath, getMimeType(ext)); 85 return `url("${dataUrl}")`; 86 }); 87 }); 88 89 return result; 90 } 91 92 function main() { 93 const opts = parseArgs(process.argv, { input: '', output: '' }); 94 if (!opts.input) { 95 console.error('Usage: node super_inline_html.js --input page.html --output page-inline.html'); 96 process.exit(1); 97 } 98 99 const inputPath = path.resolve(opts.input); 100 if (!fs.existsSync(inputPath)) { 101 console.error(`Error: Input file not found: ${inputPath}`); 102 process.exit(1); 103 } 104 105 const html = fs.readFileSync(inputPath, 'utf-8'); 106 const baseDir = path.dirname(inputPath); 107 const inlined = inlineHtml(html, baseDir); 108 109 if (opts.output) { 110 const outputPath = path.resolve(opts.output); 111 fs.writeFileSync(outputPath, inlined, 'utf-8'); 112 console.log(`Inlined HTML saved to: ${outputPath}`); 113 } else { 114 process.stdout.write(inlined); 115 } 116 } 117 118 main();