Event-based plugin system for extending highlight.js functionality with custom processing, formatting, and integration capabilities.
Register a new plugin to extend highlighting behavior.
/**
* Add a plugin to the highlighter
* @param plugin - Plugin object with event handlers
*/
function addPlugin(plugin: HLJSPlugin): void;
interface HLJSPlugin {
/** Called before highlighting starts */
'before:highlight'?: (context: BeforeHighlightContext) => void;
/** Called after highlighting completes */
'after:highlight'?: (result: HighlightResult) => void;
/** Called before highlighting a DOM element */
'before:highlightElement'?: (data: {el: Element, language: string}) => void;
/** Called after highlighting a DOM element */
'after:highlightElement'?: (data: {el: Element, result: HighlightResult, text: string}) => void;
/** @deprecated Use 'before:highlightElement' - will be removed in v12 */
'before:highlightBlock'?: (data: {block: Element, language: string}) => void;
/** @deprecated Use 'after:highlightElement' - will be removed in v12 */
'after:highlightBlock'?: (data: {block: Element, result: HighlightResult, text: string}) => void;
}Usage Examples:
import hljs from 'highlight.js';
// Simple logging plugin
const loggingPlugin = {
'before:highlight': (context) => {
console.log('About to highlight:', context.language, context.code.slice(0, 50));
},
'after:highlight': (result) => {
console.log('Highlighted as:', result.language, 'relevance:', result.relevance);
}
};
hljs.addPlugin(loggingPlugin);
// Performance monitoring plugin
const performancePlugin = {
'before:highlight': (context) => {
context.startTime = performance.now();
},
'after:highlight': (result) => {
const duration = performance.now() - result.startTime;
console.log(`Highlighting took ${duration.toFixed(2)}ms`);
}
};
hljs.addPlugin(performancePlugin);
// Code transformation plugin
const transformPlugin = {
'before:highlight': (context) => {
// Normalize line endings
context.code = context.code.replace(/\r\n/g, '\n');
// Remove trailing whitespace
context.code = context.code.replace(/[ \t]+$/gm, '');
},
'after:highlight': (result) => {
// Add line numbers
const lines = result.value.split('\n');
result.value = lines.map((line, index) =>
`<span class="line-number">${index + 1}</span>${line}`
).join('\n');
}
};
hljs.addPlugin(transformPlugin);Unregister a previously added plugin.
/**
* Remove a plugin from the highlighter
* @param plugin - Plugin object to remove (must be same reference)
*/
function removePlugin(plugin: HLJSPlugin): void;Usage Examples:
// Remove specific plugin
hljs.removePlugin(loggingPlugin);
// Conditional plugin management
let debugPlugin = null;
function enableDebugMode() {
if (!debugPlugin) {
debugPlugin = {
'before:highlight': (context) => {
console.log('Debug: highlighting', context.language);
},
'after:highlight': (result) => {
if (result.errorRaised) {
console.error('Debug: error during highlighting:', result.errorRaised);
}
}
};
hljs.addPlugin(debugPlugin);
}
}
function disableDebugMode() {
if (debugPlugin) {
hljs.removePlugin(debugPlugin);
debugPlugin = null;
}
}
// Plugin registry for management
class PluginManager {
constructor() {
this.plugins = new Map();
}
register(name, plugin) {
if (this.plugins.has(name)) {
this.unregister(name);
}
this.plugins.set(name, plugin);
hljs.addPlugin(plugin);
}
unregister(name) {
const plugin = this.plugins.get(name);
if (plugin) {
hljs.removePlugin(plugin);
this.plugins.delete(name);
}
}
clear() {
for (const [name, plugin] of this.plugins) {
hljs.removePlugin(plugin);
}
this.plugins.clear();
}
}Triggered before the highlighting process begins.
interface BeforeHighlightContext {
/** Code string to be highlighted */
code: string;
/** Target language for highlighting */
language: string;
/** Result object (may be undefined initially) */
result?: HighlightResult;
}Event Handler Examples:
const preprocessorPlugin = {
'before:highlight': (context) => {
// Code preprocessing
if (context.language === 'javascript') {
// Remove console.log statements for cleaner examples
context.code = context.code.replace(/console\.log\([^)]*\);?\s*/g, '');
}
// Language aliasing
if (context.language === 'js') {
context.language = 'javascript';
}
// Code normalization
context.code = context.code.trim();
}
};
const securityPlugin = {
'before:highlight': (context) => {
// Security: check for potentially malicious patterns
const dangerousPatterns = [
/<script[^>]*>/i,
/javascript:/i,
/data:text\/html/i
];
for (const pattern of dangerousPatterns) {
if (pattern.test(context.code)) {
console.warn('Potentially dangerous code detected');
context.code = context.code.replace(pattern, '[FILTERED]');
}
}
}
};Triggered after highlighting is complete.
interface HighlightResult {
/** Highlighted HTML output */
value: string;
/** Detected or specified language */
language?: string;
/** Confidence score */
relevance: number;
/** Whether illegal constructs were found */
illegal: boolean;
/** Any error that occurred */
errorRaised?: Error;
/** Second-best detection result (auto-detection only) */
secondBest?: Omit<HighlightResult, 'secondBest'>;
}Event Handler Examples:
const postProcessorPlugin = {
'after:highlight': (result) => {
// Add custom CSS classes
result.value = result.value.replace(
/<span class="hljs-keyword">/g,
'<span class="hljs-keyword syntax-keyword">'
);
// Add data attributes
result.value = `<div data-language="${result.language}" data-relevance="${result.relevance}">${result.value}</div>`;
}
};
const analyticsPlugin = {
'after:highlight': (result) => {
// Track highlighting usage
if (typeof gtag !== 'undefined') {
gtag('event', 'code_highlight', {
event_category: 'syntax_highlighting',
event_label: result.language,
value: result.relevance
});
}
// Performance monitoring
if (result.language && result.relevance < 3) {
console.warn(`Low confidence highlighting for ${result.language}: ${result.relevance}`);
}
}
};
const errorHandlingPlugin = {
'after:highlight': (result) => {
if (result.errorRaised) {
// Log errors to monitoring service
console.error('Highlighting error:', {
language: result.language,
error: result.errorRaised.message,
stack: result.errorRaised.stack
});
// Provide fallback
result.value = `<pre class="error">Error highlighting ${result.language} code</pre>`;
}
}
};Events for DOM-based highlighting operations.
interface ElementEventData {
/** DOM element being highlighted */
el: Element;
/** Detected or specified language */
language: string;
}
interface ElementResultData {
/** DOM element that was highlighted */
el: Element;
/** Highlighting result */
result: HighlightResult;
/** Original text content */
text: string;
}Event Handler Examples:
const domPlugin = {
'before:highlightElement': (data) => {
// Add loading indicator
data.el.classList.add('highlighting');
// Store original content
data.el.dataset.originalText = data.el.textContent;
// Language detection from data attributes
if (!data.language && data.el.dataset.language) {
data.language = data.el.dataset.language;
}
},
'after:highlightElement': (data) => {
// Remove loading indicator
data.el.classList.remove('highlighting');
data.el.classList.add('highlighted');
// Add metadata
data.el.dataset.highlightedLanguage = data.result.language;
data.el.dataset.relevance = data.result.relevance.toString();
// Add copy button
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
copyButton.className = 'copy-code-button';
copyButton.onclick = () => {
navigator.clipboard.writeText(data.text);
};
const container = data.el.closest('pre') || data.el;
container.style.position = 'relative';
copyButton.style.position = 'absolute';
copyButton.style.top = '5px';
copyButton.style.right = '5px';
container.appendChild(copyButton);
}
};
hljs.addPlugin(domPlugin);const themingPlugin = {
'after:highlight': (result) => {
// Apply theme-specific transformations
const theme = document.body.dataset.theme || 'light';
if (theme === 'dark') {
result.value = result.value.replace(
/hljs-/g,
'hljs-dark-'
);
}
}
};const lineNumbersPlugin = {
'after:highlightElement': (data) => {
const lines = data.result.value.split('\n');
const numberedLines = lines.map((line, index) =>
`<span class="line-number" data-line="${index + 1}"></span>${line}`
).join('\n');
data.el.innerHTML = numberedLines;
data.el.classList.add('line-numbers');
}
};const languageBadgePlugin = {
'after:highlightElement': (data) => {
if (data.result.language) {
const badge = document.createElement('span');
badge.className = 'language-badge';
badge.textContent = data.result.language;
const container = data.el.closest('pre') || data.el;
container.style.position = 'relative';
badge.style.position = 'absolute';
badge.style.top = '0';
badge.style.right = '0';
badge.style.padding = '2px 8px';
badge.style.backgroundColor = '#333';
badge.style.color = '#fff';
badge.style.fontSize = '12px';
container.appendChild(badge);
}
}
};const syntaxErrorPlugin = {
'after:highlight': (result) => {
if (result.illegal && result.language) {
console.warn(`Syntax errors detected in ${result.language} code`);
// Add warning indicator
result.value = `<div class="syntax-warning">⚠️ Syntax errors detected</div>${result.value}`;
}
}
};