489 lines
16 KiB
Text
489 lines
16 KiB
Text
|
|
#!/usr/bin/env nu
|
||
|
|
# Highlight.js syntax highlighting optimizer
|
||
|
|
# Automatically detects code blocks and includes necessary highlight.js assets
|
||
|
|
|
||
|
|
# Main highlight.js processing function
|
||
|
|
export def process_highlight [html_content: string, config: record, dev: bool, verbose: bool] {
|
||
|
|
let highlight_config = if "highlight" in ($config | columns) { $config.highlight } else { { enabled: false } }
|
||
|
|
|
||
|
|
# Handle different enabled values: "auto", true, false
|
||
|
|
let is_enabled = match $highlight_config.enabled {
|
||
|
|
"auto" => true # Will be determined by auto-detection
|
||
|
|
true => true
|
||
|
|
false => false
|
||
|
|
_ => false
|
||
|
|
}
|
||
|
|
|
||
|
|
if not $is_enabled {
|
||
|
|
if $verbose { print "🖍️ Syntax highlighting disabled, skipping..." }
|
||
|
|
return {
|
||
|
|
html: $html_content,
|
||
|
|
css_assets: [],
|
||
|
|
js_assets: [],
|
||
|
|
size: 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if $verbose { print "🖍️ Processing syntax highlighting..." }
|
||
|
|
|
||
|
|
# Auto-detect code blocks if enabled
|
||
|
|
let auto_detect = if "auto_detect" in ($highlight_config | columns) { $highlight_config.auto_detect } else { true }
|
||
|
|
let detected_languages = if $auto_detect {
|
||
|
|
detect_code_languages $html_content $verbose
|
||
|
|
} else {
|
||
|
|
if "languages" in ($highlight_config | columns) { $highlight_config.languages } else { [] }
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($detected_languages | length) == 0 {
|
||
|
|
if $verbose { print " ⚠️ No code blocks detected, skipping highlight.js" }
|
||
|
|
# Return original HTML without changes
|
||
|
|
return {
|
||
|
|
html: $html_content,
|
||
|
|
css_assets: [],
|
||
|
|
js_assets: [],
|
||
|
|
size: 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# For "auto" mode, only proceed if we actually detected code blocks
|
||
|
|
if $highlight_config.enabled == "auto" and ($detected_languages | length) == 0 {
|
||
|
|
return {
|
||
|
|
html: $html_content,
|
||
|
|
css_assets: [],
|
||
|
|
js_assets: [],
|
||
|
|
size: 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if $verbose { print $" 📝 Detected languages: ($detected_languages | str join ', ')" }
|
||
|
|
|
||
|
|
# Prepare highlight.js assets
|
||
|
|
let assets = (prepare_highlight_assets $detected_languages $highlight_config $dev $verbose)
|
||
|
|
|
||
|
|
# Enhance HTML with highlight.js integration
|
||
|
|
let enhanced_html = (enhance_html_with_highlight $html_content $highlight_config $verbose)
|
||
|
|
|
||
|
|
{
|
||
|
|
html: $enhanced_html,
|
||
|
|
css_assets: $assets.css_assets,
|
||
|
|
js_assets: $assets.js_assets,
|
||
|
|
size: $assets.total_size
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Detect programming languages in HTML code blocks
|
||
|
|
def detect_code_languages [html_content: string, verbose: bool] {
|
||
|
|
mut languages = []
|
||
|
|
|
||
|
|
# Check for code blocks with language classes
|
||
|
|
let has_code_blocks = ($html_content | str contains '<code' or ($html_content | str contains '<pre'))
|
||
|
|
|
||
|
|
if not $has_code_blocks {
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
|
||
|
|
# Simple language detection from common patterns
|
||
|
|
if ($html_content | str contains 'language-rust') { $languages = ($languages | append 'rust') }
|
||
|
|
if ($html_content | str contains 'language-javascript') { $languages = ($languages | append 'javascript') }
|
||
|
|
if ($html_content | str contains 'language-typescript') { $languages = ($languages | append 'typescript') }
|
||
|
|
if ($html_content | str contains 'language-python') { $languages = ($languages | append 'python') }
|
||
|
|
if ($html_content | str contains 'language-bash') { $languages = ($languages | append 'bash') }
|
||
|
|
if ($html_content | str contains 'language-json') { $languages = ($languages | append 'json') }
|
||
|
|
if ($html_content | str contains 'language-html') { $languages = ($languages | append 'html') }
|
||
|
|
if ($html_content | str contains 'language-css') { $languages = ($languages | append 'css') }
|
||
|
|
if ($html_content | str contains 'language-toml') { $languages = ($languages | append 'toml') }
|
||
|
|
if ($html_content | str contains 'language-yaml') { $languages = ($languages | append 'yaml') }
|
||
|
|
|
||
|
|
# Also check for lang- variants
|
||
|
|
if ($html_content | str contains 'lang-rust') { $languages = ($languages | append 'rust') }
|
||
|
|
if ($html_content | str contains 'lang-js') { $languages = ($languages | append 'javascript') }
|
||
|
|
if ($html_content | str contains 'lang-javascript') { $languages = ($languages | append 'javascript') }
|
||
|
|
if ($html_content | str contains 'lang-ts') { $languages = ($languages | append 'typescript') }
|
||
|
|
if ($html_content | str contains 'lang-py') { $languages = ($languages | append 'python') }
|
||
|
|
if ($html_content | str contains 'lang-python') { $languages = ($languages | append 'python') }
|
||
|
|
if ($html_content | str contains 'lang-sh') { $languages = ($languages | append 'bash') }
|
||
|
|
if ($html_content | str contains 'lang-bash') { $languages = ($languages | append 'bash') }
|
||
|
|
|
||
|
|
# Also check for generic code blocks
|
||
|
|
let has_generic_code = ($html_content | str contains '<pre><code>' or ($html_content | str contains '<code>'))
|
||
|
|
if $has_generic_code and ($languages | length) == 0 {
|
||
|
|
# Default to common languages for generic code blocks
|
||
|
|
$languages = ["javascript", "bash", "json"]
|
||
|
|
if $verbose { print " 💡 Found generic code blocks, assuming common languages" }
|
||
|
|
}
|
||
|
|
|
||
|
|
# Normalize language names to highlight.js format
|
||
|
|
$languages | each { |lang|
|
||
|
|
match $lang {
|
||
|
|
"js" => "javascript"
|
||
|
|
"ts" => "typescript"
|
||
|
|
"sh" => "bash"
|
||
|
|
"shell" => "bash"
|
||
|
|
"yml" => "yaml"
|
||
|
|
"py" => "python"
|
||
|
|
_ => $lang
|
||
|
|
}
|
||
|
|
} | uniq
|
||
|
|
}
|
||
|
|
|
||
|
|
# Prepare highlight.js assets based on detected languages
|
||
|
|
def prepare_highlight_assets [languages: list, highlight_config: record, dev: bool, verbose: bool] {
|
||
|
|
let theme = if "theme" in ($highlight_config | columns) { $highlight_config.theme } else { "github-dark" }
|
||
|
|
let use_cdn = if "cdn" in ($highlight_config | columns) { $highlight_config.cdn } else { false }
|
||
|
|
let copy_button = if "copy_button" in ($highlight_config | columns) { $highlight_config.copy_button } else { true }
|
||
|
|
|
||
|
|
mut css_assets = []
|
||
|
|
mut js_assets = []
|
||
|
|
mut total_size = 0
|
||
|
|
|
||
|
|
if $use_cdn {
|
||
|
|
# Use CDN assets
|
||
|
|
let cdn_url = if "cdn_url" in ($highlight_config | columns) {
|
||
|
|
$highlight_config.cdn_url
|
||
|
|
} else {
|
||
|
|
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add CDN CSS
|
||
|
|
$css_assets = ($css_assets | append {
|
||
|
|
type: "cdn",
|
||
|
|
url: $"($cdn_url)styles/($theme).min.css",
|
||
|
|
inline: false
|
||
|
|
})
|
||
|
|
|
||
|
|
# Add CDN JS
|
||
|
|
$js_assets = ($js_assets | append {
|
||
|
|
type: "cdn",
|
||
|
|
url: $"($cdn_url)highlight.min.js",
|
||
|
|
inline: false
|
||
|
|
})
|
||
|
|
|
||
|
|
# Add language-specific modules
|
||
|
|
for lang in $languages {
|
||
|
|
$js_assets = ($js_assets | append {
|
||
|
|
type: "cdn",
|
||
|
|
url: $"($cdn_url)languages/($lang).min.js",
|
||
|
|
inline: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
if $verbose { print $" 🌐 Using CDN assets for theme: ($theme)" }
|
||
|
|
} else {
|
||
|
|
# Use local assets - download and bundle
|
||
|
|
let assets_result = (download_highlight_assets $languages $theme $dev $verbose)
|
||
|
|
$css_assets = $assets_result.css_assets
|
||
|
|
$js_assets = $assets_result.js_assets
|
||
|
|
$total_size = $assets_result.total_size
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add copy button functionality if enabled
|
||
|
|
if $copy_button {
|
||
|
|
let copy_js = (generate_copy_button_script)
|
||
|
|
$js_assets = ($js_assets | append {
|
||
|
|
type: "inline",
|
||
|
|
content: $copy_js,
|
||
|
|
inline: true
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add highlight.js initialization
|
||
|
|
let init_script = (generate_highlight_init_script $highlight_config)
|
||
|
|
$js_assets = ($js_assets | append {
|
||
|
|
type: "inline",
|
||
|
|
content: $init_script,
|
||
|
|
inline: true
|
||
|
|
})
|
||
|
|
|
||
|
|
{
|
||
|
|
css_assets: $css_assets,
|
||
|
|
js_assets: $js_assets,
|
||
|
|
total_size: $total_size
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Download and prepare local highlight.js assets
|
||
|
|
def download_highlight_assets [languages: list, theme: string, dev: bool, verbose: bool] {
|
||
|
|
# Create vendor directory for highlight.js
|
||
|
|
let vendor_dir = "shared/vendor/highlight"
|
||
|
|
mkdir $vendor_dir
|
||
|
|
mkdir ($vendor_dir | path join "styles")
|
||
|
|
mkdir ($vendor_dir | path join "languages")
|
||
|
|
|
||
|
|
mut css_assets = []
|
||
|
|
mut js_assets = []
|
||
|
|
mut total_size = 0
|
||
|
|
|
||
|
|
let base_url = "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build"
|
||
|
|
|
||
|
|
# Download theme CSS
|
||
|
|
let theme_css_path = ($vendor_dir | path join "styles" $"($theme).min.css")
|
||
|
|
if not ($theme_css_path | path exists) {
|
||
|
|
if $verbose { print $" 📥 Downloading theme: ($theme)" }
|
||
|
|
try {
|
||
|
|
http get $"($base_url)/styles/($theme).min.css" | save $theme_css_path
|
||
|
|
} catch {
|
||
|
|
if $verbose { print $" ⚠️ Failed to download theme ($theme), using github-dark fallback" }
|
||
|
|
try {
|
||
|
|
http get $"($base_url)/styles/github-dark.min.css" | save $theme_css_path
|
||
|
|
} catch {
|
||
|
|
if $verbose { print $" ❌ Failed to download highlight.js theme" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($theme_css_path | path exists) {
|
||
|
|
let css_size = (ls $theme_css_path | get size | first | into int)
|
||
|
|
$total_size = ($total_size + $css_size)
|
||
|
|
$css_assets = ($css_assets | append {
|
||
|
|
type: "local",
|
||
|
|
path: $theme_css_path,
|
||
|
|
inline: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
# Download core highlight.js
|
||
|
|
let core_js_path = ($vendor_dir | path join "highlight.min.js")
|
||
|
|
if not ($core_js_path | path exists) {
|
||
|
|
if $verbose { print " 📥 Downloading highlight.js core" }
|
||
|
|
try {
|
||
|
|
http get $"($base_url)/highlight.min.js" | save $core_js_path
|
||
|
|
} catch {
|
||
|
|
if $verbose { print " ❌ Failed to download highlight.js core" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($core_js_path | path exists) {
|
||
|
|
let js_size = (ls $core_js_path | get size | first | into int)
|
||
|
|
$total_size = ($total_size + $js_size)
|
||
|
|
$js_assets = ($js_assets | append {
|
||
|
|
type: "local",
|
||
|
|
path: $core_js_path,
|
||
|
|
inline: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
# Download language modules
|
||
|
|
for lang in $languages {
|
||
|
|
let lang_js_path = ($vendor_dir | path join "languages" $"($lang).min.js")
|
||
|
|
if not ($lang_js_path | path exists) {
|
||
|
|
if $verbose { print $" 📥 Downloading language: ($lang)" }
|
||
|
|
try {
|
||
|
|
http get $"($base_url)/languages/($lang).min.js" | save $lang_js_path
|
||
|
|
} catch {
|
||
|
|
if $verbose { print $" ⚠️ Failed to download language: ($lang)" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($lang_js_path | path exists) {
|
||
|
|
let lang_size = (ls $lang_js_path | get size | first | into int)
|
||
|
|
$total_size = ($total_size + $lang_size)
|
||
|
|
$js_assets = ($js_assets | append {
|
||
|
|
type: "local",
|
||
|
|
path: $lang_js_path,
|
||
|
|
inline: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
css_assets: $css_assets,
|
||
|
|
js_assets: $js_assets,
|
||
|
|
total_size: $total_size
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Enhance HTML with highlight.js classes and copy buttons
|
||
|
|
def enhance_html_with_highlight [html_content: string, highlight_config: record, verbose: bool] {
|
||
|
|
let copy_button = if "copy_button" in ($highlight_config | columns) { $highlight_config.copy_button } else { true }
|
||
|
|
let line_numbers = if "line_numbers" in ($highlight_config | columns) { $highlight_config.line_numbers } else { false }
|
||
|
|
|
||
|
|
mut enhanced_html = $html_content
|
||
|
|
|
||
|
|
# Add copy buttons to code blocks if enabled
|
||
|
|
if $copy_button {
|
||
|
|
# Replace <pre><code> blocks with enhanced version
|
||
|
|
$enhanced_html = ($enhanced_html | str replace -ar '<pre><code([^>]*)>' '<div class="code-block-container"><button class="copy-button" onclick="copyCode(this)" title="Copy code">📋</button><pre><code$1>')
|
||
|
|
$enhanced_html = ($enhanced_html | str replace -ar '</code></pre>' '</code></pre></div>')
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add line numbers wrapper if enabled
|
||
|
|
if $line_numbers {
|
||
|
|
$enhanced_html = ($enhanced_html | str replace -ar '<pre><code' '<pre class="line-numbers"><code')
|
||
|
|
}
|
||
|
|
|
||
|
|
# Ensure all code blocks have proper classes for highlight.js
|
||
|
|
$enhanced_html = ($enhanced_html | str replace -ar '<code(?![^>]*class=)' '<code class="hljs"')
|
||
|
|
|
||
|
|
if $verbose { print $" ✅ Enhanced HTML with highlight.js integration" }
|
||
|
|
|
||
|
|
$enhanced_html
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate copy button JavaScript
|
||
|
|
def generate_copy_button_script [] {
|
||
|
|
"
|
||
|
|
function copyCode(button) {
|
||
|
|
const codeBlock = button.nextElementSibling.querySelector('code');
|
||
|
|
const text = codeBlock.textContent;
|
||
|
|
|
||
|
|
navigator.clipboard.writeText(text).then(() => {
|
||
|
|
const originalText = button.textContent;
|
||
|
|
button.textContent = '✅';
|
||
|
|
button.style.color = '#10b981';
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
button.textContent = originalText;
|
||
|
|
button.style.color = '';
|
||
|
|
}, 2000);
|
||
|
|
}).catch(() => {
|
||
|
|
// Fallback for older browsers
|
||
|
|
const textArea = document.createElement('textarea');
|
||
|
|
textArea.value = text;
|
||
|
|
document.body.appendChild(textArea);
|
||
|
|
textArea.select();
|
||
|
|
document.execCommand('copy');
|
||
|
|
document.body.removeChild(textArea);
|
||
|
|
|
||
|
|
const originalText = button.textContent;
|
||
|
|
button.textContent = '✅';
|
||
|
|
setTimeout(() => {
|
||
|
|
button.textContent = originalText;
|
||
|
|
}, 2000);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate highlight.js initialization script
|
||
|
|
def generate_highlight_init_script [highlight_config: record] {
|
||
|
|
let line_numbers = if "line_numbers" in ($highlight_config | columns) { $highlight_config.line_numbers } else { false }
|
||
|
|
|
||
|
|
let init_code = if $line_numbers {
|
||
|
|
"
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
hljs.highlightAll();
|
||
|
|
hljs.initLineNumbersOnLoad();
|
||
|
|
});
|
||
|
|
"
|
||
|
|
} else {
|
||
|
|
"
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
hljs.highlightAll();
|
||
|
|
});
|
||
|
|
"
|
||
|
|
}
|
||
|
|
|
||
|
|
$init_code
|
||
|
|
}
|
||
|
|
|
||
|
|
# Inject highlight.js assets into HTML head and body
|
||
|
|
export def inject_highlight_assets [html_content: string, css_assets: list, js_assets: list] {
|
||
|
|
mut updated_html = $html_content
|
||
|
|
|
||
|
|
# Inject CSS in head
|
||
|
|
let css_tags = ($css_assets | each { |asset|
|
||
|
|
match $asset.type {
|
||
|
|
"cdn" => $"<link rel=\"stylesheet\" href=\"($asset.url)\">"
|
||
|
|
"local" => $"<link rel=\"stylesheet\" href=\"($asset.path)\">"
|
||
|
|
"inline" => $"<style>($asset.content)</style>"
|
||
|
|
}
|
||
|
|
} | str join "\n")
|
||
|
|
|
||
|
|
if ($css_tags | str length) > 0 {
|
||
|
|
$updated_html = ($updated_html | str replace '</head>' $"($css_tags)\n</head>")
|
||
|
|
}
|
||
|
|
|
||
|
|
# Inject JS before </body>
|
||
|
|
let js_tags = ($js_assets | each { |asset|
|
||
|
|
match $asset.type {
|
||
|
|
"cdn" => $"<script src=\"($asset.url)\"></script>"
|
||
|
|
"local" => $"<script src=\"($asset.path)\"></script>"
|
||
|
|
"inline" => $"<script>($asset.content)</script>"
|
||
|
|
}
|
||
|
|
} | str join "\n")
|
||
|
|
|
||
|
|
if ($js_tags | str length) > 0 {
|
||
|
|
$updated_html = ($updated_html | str replace '</body>' $"($js_tags)\n</body>")
|
||
|
|
}
|
||
|
|
|
||
|
|
$updated_html
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate additional CSS for highlight.js styling
|
||
|
|
export def generate_highlight_css [highlight_config: record] {
|
||
|
|
let copy_button = if "copy_button" in ($highlight_config | columns) { $highlight_config.copy_button } else { true }
|
||
|
|
|
||
|
|
mut css = ""
|
||
|
|
|
||
|
|
if $copy_button {
|
||
|
|
$css = $css + "
|
||
|
|
/* Highlight.js code block styling with copy button */
|
||
|
|
.code-block-container {
|
||
|
|
position: relative;
|
||
|
|
margin: 1rem 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.copy-button {
|
||
|
|
position: absolute;
|
||
|
|
top: 0.5rem;
|
||
|
|
right: 0.5rem;
|
||
|
|
background: rgba(255, 255, 255, 0.1);
|
||
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
|
|
border-radius: 4px;
|
||
|
|
color: #fff;
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
padding: 0.25rem 0.5rem;
|
||
|
|
transition: all 0.2s ease;
|
||
|
|
z-index: 10;
|
||
|
|
}
|
||
|
|
|
||
|
|
.copy-button:hover {
|
||
|
|
background: rgba(255, 255, 255, 0.2);
|
||
|
|
border-color: rgba(255, 255, 255, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block-container pre {
|
||
|
|
margin: 0;
|
||
|
|
padding-top: 2.5rem;
|
||
|
|
}
|
||
|
|
"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add general code styling
|
||
|
|
$css = $css + "
|
||
|
|
/* General code block styling */
|
||
|
|
pre code.hljs {
|
||
|
|
border-radius: 8px;
|
||
|
|
overflow-x: auto;
|
||
|
|
padding: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
code:not(pre code) {
|
||
|
|
background: rgba(255, 255, 255, 0.1);
|
||
|
|
border-radius: 3px;
|
||
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
|
|
font-size: 0.85em;
|
||
|
|
padding: 0.2em 0.4em;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Line numbers styling */
|
||
|
|
pre.line-numbers {
|
||
|
|
position: relative;
|
||
|
|
padding-left: 3em;
|
||
|
|
}
|
||
|
|
|
||
|
|
pre.line-numbers::before {
|
||
|
|
content: counter(line-number);
|
||
|
|
counter-increment: line-number;
|
||
|
|
position: absolute;
|
||
|
|
left: 0;
|
||
|
|
color: rgba(255, 255, 255, 0.4);
|
||
|
|
text-align: right;
|
||
|
|
width: 2.5em;
|
||
|
|
padding-right: 0.5em;
|
||
|
|
}
|
||
|
|
"
|
||
|
|
|
||
|
|
$css
|
||
|
|
}
|