Extensible plugin architecture for customizing highlighting behavior, adding functionality, and hooking into the highlighting lifecycle.
Adds a plugin to extend highlight.js functionality.
/**
* Adds a plugin to the highlighter
* @param plugin - Plugin object with event handler methods
*/
function addPlugin(plugin: HLJSPlugin): void;
interface HLJSPlugin {
/** Called after highlighting is complete */
'after:highlight'?: (result: HighlightResult) => void;
/** Called before highlighting starts */
'before:highlight'?: (context: BeforeHighlightContext) => void;
/** Called after highlighting a DOM element */
'after:highlightElement'?: (data: {
el: Element;
result: HighlightResult;
text: string
}) => void;
/** Called before highlighting a DOM element */
'before:highlightElement'?: (data: {
el: Element;
language: string
}) => void;
/** @deprecated Legacy event, use 'after:highlightElement' */
'after:highlightBlock'?: (data: {
block: Element;
result: HighlightResult;
text: string
}) => void;
/** @deprecated Legacy event, use 'before:highlightElement' */
'before:highlightBlock'?: (data: {
block: Element;
language: string
}) => void;
}
interface BeforeHighlightContext {
/** Code string to be highlighted */
code: string;
/** Language name for highlighting */
language: string;
/** Partial result object (may be modified) */
result?: HighlightResult;
}Usage Examples:
import hljs from '@highlightjs/cdn-assets/es/highlight.js';
// Simple logging plugin
const loggingPlugin = {
'before:highlight': (context) => {
console.log(`Highlighting ${context.code.length} chars as ${context.language}`);
},
'after:highlight': (result) => {
console.log(`Highlighted with relevance ${result.relevance}`);
}
};
hljs.addPlugin(loggingPlugin);
// Line numbers plugin
const lineNumbersPlugin = {
'after:highlightElement': ({ el, result }) => {
const lines = result.value.split('\n');
const numberedLines = lines.map((line, i) =>
`<span class="line-number">${i + 1}</span>${line}`
).join('\n');
el.innerHTML = numberedLines;
}
};
hljs.addPlugin(lineNumbersPlugin);Removes a previously added plugin.
/**
* Removes a plugin from the highlighter
* @param plugin - Plugin object to remove (must be same reference)
*/
function removePlugin(plugin: HLJSPlugin): void;Usage Examples:
// Remove a specific plugin
hljs.removePlugin(loggingPlugin);
// Plugin management system
class PluginManager {
constructor() {
this.plugins = new Map();
}
addPlugin(name, plugin) {
this.plugins.set(name, plugin);
hljs.addPlugin(plugin);
}
removePlugin(name) {
const plugin = this.plugins.get(name);
if (plugin) {
hljs.removePlugin(plugin);
this.plugins.delete(name);
}
}
hasPlugin(name) {
return this.plugins.has(name);
}
}
const manager = new PluginManager();
manager.addPlugin('logger', loggingPlugin);
manager.removePlugin('logger');const copyButtonPlugin = {
'after:highlightElement': ({ el }) => {
// Create copy button
const button = document.createElement('button');
button.textContent = 'Copy';
button.className = 'copy-btn';
// Add click handler
button.addEventListener('click', () => {
const code = el.textContent;
navigator.clipboard.writeText(code).then(() => {
button.textContent = 'Copied!';
setTimeout(() => button.textContent = 'Copy', 2000);
});
});
// Add button to element container
const container = el.parentElement;
if (container.tagName === 'PRE') {
container.style.position = 'relative';
button.style.position = 'absolute';
button.style.top = '5px';
button.style.right = '5px';
container.appendChild(button);
}
}
};
hljs.addPlugin(copyButtonPlugin);const languageBadgePlugin = {
'after:highlightElement': ({ el, result }) => {
if (result.language) {
const badge = document.createElement('span');
badge.className = 'language-badge';
badge.textContent = result.language;
badge.style.cssText = `
position: absolute;
top: 0;
right: 0;
background: #333;
color: white;
padding: 2px 6px;
font-size: 0.8em;
border-radius: 0 0 0 4px;
`;
const container = el.parentElement;
if (container.tagName === 'PRE') {
container.style.position = 'relative';
container.appendChild(badge);
}
}
}
};
hljs.addPlugin(languageBadgePlugin);const statsPlugin = {
'after:highlight': (result) => {
// Track statistics
if (!window.hljsStats) {
window.hljsStats = {
totalHighlights: 0,
languageCount: {},
averageRelevance: []
};
}
const stats = window.hljsStats;
stats.totalHighlights++;
stats.averageRelevance.push(result.relevance);
if (result.language) {
stats.languageCount[result.language] =
(stats.languageCount[result.language] || 0) + 1;
}
}
};
hljs.addPlugin(statsPlugin);
// View statistics
function getHighlightingStats() {
const stats = window.hljsStats;
if (!stats) return null;
return {
total: stats.totalHighlights,
languages: stats.languageCount,
avgRelevance: stats.averageRelevance.reduce((a, b) => a + b, 0) / stats.averageRelevance.length
};
}const themeSwitcherPlugin = {
'after:highlightElement': ({ el }) => {
// Add theme class for styling
el.classList.add('hljs-themed');
// Add data attribute for theme switching
el.dataset.theme = document.body.dataset.theme || 'default';
}
};
hljs.addPlugin(themeSwitcherPlugin);
// Theme switching function
function switchTheme(themeName) {
document.body.dataset.theme = themeName;
// Update all highlighted elements
document.querySelectorAll('.hljs-themed').forEach(el => {
el.dataset.theme = themeName;
});
// Load theme CSS
const existingLink = document.querySelector('link[data-hljs-theme]');
if (existingLink) {
existingLink.remove();
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `@highlightjs/cdn-assets/styles/${themeName}.css`;
link.dataset.hljsTheme = 'true';
document.head.appendChild(link);
}const performancePlugin = {
'before:highlight': (context) => {
context.startTime = performance.now();
},
'after:highlight': (result) => {
if (result.startTime) {
const duration = performance.now() - result.startTime;
console.log(`Highlighting took ${duration.toFixed(2)}ms`);
// Track slow highlights
if (duration > 50) {
console.warn(`Slow highlighting detected: ${duration.toFixed(2)}ms for ${result.language}`);
}
}
}
};
hljs.addPlugin(performancePlugin);const customHighlightPlugin = {
'before:highlight': (context) => {
// Pre-process code before highlighting
if (context.language === 'sql') {
// Normalize SQL keywords to uppercase
context.code = context.code.replace(/\b(select|from|where|insert|update|delete)\b/gi,
(match) => match.toUpperCase()
);
}
},
'after:highlight': (result) => {
// Post-process highlighted HTML
if (result.language === 'javascript') {
// Add special styling for console.log
result.value = result.value.replace(
/(console\.log)/g,
'<span class="console-method">$1</span>'
);
}
}
};
hljs.addPlugin(customHighlightPlugin);// Plugin template with error handling
const robustPlugin = {
'after:highlightElement': ({ el, result, text }) => {
try {
// Plugin logic here
console.log('Processing element:', el);
} catch (error) {
console.error('Plugin error:', error);
// Don't break highlighting for other plugins
}
}
};
// Plugin with configuration options
function createConfigurablePlugin(options = {}) {
const config = {
showLineNumbers: true,
showCopyButton: true,
...options
};
return {
'after:highlightElement': ({ el, result }) => {
if (config.showLineNumbers) {
// Add line numbers
}
if (config.showCopyButton) {
// Add copy button
}
}
};
}
// Usage
const myPlugin = createConfigurablePlugin({
showLineNumbers: false,
showCopyButton: true
});
hljs.addPlugin(myPlugin);1. before:highlightElement (for DOM highlighting)
2. before:highlight (for all highlighting)
3. [highlighting process]
4. after:highlight (for all highlighting)
5. after:highlightElement (for DOM highlighting)Multiple plugins are called in the order they were added.