website-htmx-rustelo-code/scripts/build/build-highlight-bundle.js

249 lines
8.4 KiB
JavaScript
Raw Permalink Normal View History

2026-07-10 03:44:13 +01:00
#!/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();