#!/usr/bin/env node /** * Build custom highlight.js bundle by downloading and combining CDN files * This creates a single local file with all required languages * * Usage: * node scripts/build-highlight-bundle.js * * This generates a bundle at public/js/highlight-bundle.min.js */ import fs from 'fs'; import path from 'path'; import https from 'https'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Discover available languages by scanning content/locales directory * This mimics the Rust discover_available_languages() function */ async function discoverAvailableLanguages() { const localesPath = process.env.SITE_I18N_PATH ? path.join(process.env.SITE_I18N_PATH, 'locales') : path.join('site', 'i18n', 'locales'); try { if (!fs.existsSync(localesPath)) { console.log(`⚠️ Locales directory not found: ${localesPath}, using fallback languages`); return ['en', 'es']; } const entries = fs.readdirSync(localesPath, { withFileTypes: true }); const languages = entries .filter(entry => entry.isDirectory()) .map(entry => entry.name) .filter(name => name.length === 2) // Only 2-letter language codes .sort(); if (languages.length === 0) { console.log(`⚠️ No language directories found in ${localesPath}, using fallback`); return ['en', 'es']; } console.log(`🌐 Discovered languages: ${languages.join(', ')}`); return languages; } catch (error) { console.log(`⚠️ Error discovering languages: ${error.message}, using fallback`); return ['en', 'es']; } } // Languages we want to include (in addition to core languages) const additionalLanguages = [ 'rust', 'typescript', 'bash', 'yaml', 'dockerfile', 'sql', 'python', 'ini', // For TOML-like syntax 'properties', // Also TOML-like syntax 'markdown', 'nix' // Closest grammar for Nickel (NCL) blocks tagged ```nix ]; const CDN_BASE = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0'; const HIGHLIGHTJS_COPY_VERSION = '1.0.6'; const COPY_PLUGIN_BASE = `https://unpkg.com/highlightjs-copy@${HIGHLIGHTJS_COPY_VERSION}/dist`; function downloadFile(url) { return new Promise((resolve, reject) => { https.get(url, (response) => { if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}: ${url}`)); return; } let data = ''; response.on('data', (chunk) => data += chunk); response.on('end', () => resolve(data)); }).on('error', reject); }); } async function buildBundle() { try { // Discover available languages first const availableLanguages = await discoverAvailableLanguages(); // Ensure output directory exists first const outputDir = path.join(__dirname, '../../site/public/js'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Check if bundle already exists and is recent (less than 24 hours old) const outputPath = path.join(outputDir, 'highlight-bundle.min.js'); if (fs.existsSync(outputPath)) { const stats = fs.statSync(outputPath); const fileAge = Date.now() - stats.mtime.getTime(); const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds if (fileAge < maxAge) { const fileSizeKB = Math.round(stats.size / 1024); const ageHours = Math.round(fileAge / (60 * 60 * 1000)); console.log('✅ Highlight.js bundle already exists and is recent!'); console.log(`📁 File: ${outputPath}`); console.log(`📊 Size: ${fileSizeKB}KB`); console.log(`⏰ Age: ${ageHours}h (created: ${stats.mtime.toLocaleString()})`); console.log('🎯 Languages: Core JS/HTML/CSS/JSON/XML + ' + additionalLanguages.join(', ')); console.log('💡 To force rebuild, delete the file or wait 24 hours'); return; } else { console.log('🔄 Bundle exists but is older than 24 hours, rebuilding...'); } } console.log('🔨 Building highlight.js bundle from CDN...'); console.log(`📦 Including core + ${additionalLanguages.length} additional languages: ${additionalLanguages.join(', ')}`); // Download core highlight.js console.log('📥 Downloading core highlight.js...'); const coreJs = await downloadFile(`${CDN_BASE}/highlight.min.js`); // Download highlightjs-copy plugin console.log('📥 Downloading highlightjs-copy plugin...'); const copyPluginJs = await downloadFile(`${COPY_PLUGIN_BASE}/highlightjs-copy.min.js`); let bundleContent = `/*! Custom Highlight.js Bundle for Rustelo * Generated on ${new Date().toISOString()} * Core + Additional Languages: ${additionalLanguages.join(', ')} * Based on Highlight.js 11.9.0 from CDN * Includes highlightjs-copy plugin v${HIGHLIGHTJS_COPY_VERSION} */ // Core highlight.js ${coreJs} // Copy button plugin ${copyPluginJs} // Additional language definitions (function() { if (typeof hljs === 'undefined') { console.error('Highlight.js core not available'); return; } `; // Download and add each language console.log('📝 Downloading language definitions...'); for (const lang of additionalLanguages) { try { console.log(` 📥 Downloading: ${lang}`); const langJs = await downloadFile(`${CDN_BASE}/languages/${lang}.min.js`); // Wrap the language code to register it properly bundleContent += ` // Language: ${lang} (function() { ${langJs} })(); `; console.log(` ✅ Added: ${lang}`); } catch (error) { console.log(` ❌ Failed to download ${lang}: ${error.message}`); } } // Close the bundle bundleContent += ` })(); // Expose available languages globally for use by other scripts if (typeof window !== 'undefined') { window.__AVAILABLE_LANGUAGES = ${JSON.stringify(availableLanguages)}; } else if (typeof globalThis !== 'undefined') { globalThis.__AVAILABLE_LANGUAGES = ${JSON.stringify(availableLanguages)}; } // Auto-initialize when DOM is ready if (typeof document !== 'undefined') { function initializeHighlightJs() { if (typeof hljs !== 'undefined' && hljs.highlightAll) { hljs.configure({ ignoreUnescapedHTML: true }); hljs.highlightAll(); // Add copy button plugin with dynamic language detection and autohide configuration if (typeof CopyButtonPlugin !== 'undefined') { const docLang = document.documentElement.lang || 'en'; // Dynamically discovered available languages from content/locales const AVAILABLE_LANGUAGES = ${JSON.stringify(availableLanguages)}; // Build language configuration for all available languages const langConfig = {}; AVAILABLE_LANGUAGES.forEach(lang => { langConfig[lang] = { autohide: false, lang: lang }; }); // Use detected language or fallback to first available language const config = langConfig[docLang] || langConfig[AVAILABLE_LANGUAGES[0]] || { autohide: false, lang: 'en' }; hljs.addPlugin(new CopyButtonPlugin(config)); } } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeHighlightJs); } else { // DOM already ready initializeHighlightJs(); } } `; // Write the bundle (directory already created at the start) fs.writeFileSync(outputPath, bundleContent); // Get file size const stats = fs.statSync(outputPath); const fileSizeKB = Math.round(stats.size / 1024); console.log(`✅ Highlight.js bundle created successfully!`); console.log(`📁 Output: ${outputPath}`); console.log(`📊 Size: ${fileSizeKB}KB`); console.log(`🎯 Languages: Core JS/HTML/CSS/JSON/XML + ${additionalLanguages.join(', ')}`); console.log(`📋 Plugin: highlightjs-copy v${HIGHLIGHTJS_COPY_VERSION} (with i18n support)`); console.log(''); console.log('🚀 Ready to use! The bundle includes:'); console.log(' - Auto-initialization on DOM ready'); console.log(' - All required languages pre-registered'); console.log(' - Copy code buttons with language detection (en/es)'); console.log(' - Single HTTP request instead of multiple CDN calls'); } catch (error) { console.error('❌ Error building bundle:', error.message); process.exit(1); } } buildBundle();