/ scripts / super_inline_html.js
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();