Plugin architecture for extending highlight.js functionality with custom processing hooks and integrations. The plugin system allows you to intercept and modify highlighting behavior at various stages of the process.
Registers a plugin with the highlighting system. Plugins can hook into various events during the highlighting process.
/**
* Adds a plugin to the highlighting system
* @param plugin - Plugin object with event handlers
*/
function addPlugin(plugin: HLJSPlugin): void;
interface HLJSPlugin {
/** Called after code highlighting is complete */
'after:highlight'?: (result: HighlightResult) => void;
/** Called before code highlighting begins */
'before:highlight'?: (context: BeforeHighlightContext) => void;
/** Called after highlighting an HTML element */
'after:highlightElement'?: (data: HighlightElementData) => void;
/** Called before highlighting an HTML element */
'before:highlightElement'?: (data: BeforeHighlightElementData) => void;
/** Called after highlighting a block (deprecated) */
'after:highlightBlock'?: (data: HighlightBlockData) => void;
/** Called before highlighting a block (deprecated) */
'before:highlightBlock'?: (data: BeforeHighlightBlockData) => void;
}
interface BeforeHighlightContext {
/** Code string to be highlighted */
code: string;
/** Language name for highlighting */
language: string;
/** Optional existing result */
result?: HighlightResult;
}
interface HighlightElementData {
/** HTML element that was highlighted */
el: Element;
/** Highlighting result */
result: HighlightResult;
/** Original text content */
text: string;
}
interface BeforeHighlightElementData {
/** HTML element to be highlighted */
el: Element;
/** Language name for highlighting */
language: string;
}
interface HighlightBlockData {
/** HTML block element that was highlighted */
block: Element;
/** Highlighting result */
result: HighlightResult;
/** Original text content */
text: string;
}
interface BeforeHighlightBlockData {
/** HTML block element to be highlighted */
block: Element;
/** Language name for highlighting */
language: string;
}Usage Examples:
import hljs from "highlight.js";
// Basic plugin for logging
const loggingPlugin = {
'before:highlight': (context) => {
console.log(`Highlighting ${context.language}:`, context.code.slice(0, 50));
},
'after:highlight': (result) => {
console.log(`Highlighted ${result.language} with relevance ${result.relevance}`);
}
};
hljs.addPlugin(loggingPlugin);
// Performance monitoring plugin
const performancePlugin = {
'before:highlight': (context) => {
context.startTime = performance.now();
},
'after:highlight': (result) => {
if (result.context && result.context.startTime) {
const duration = performance.now() - result.context.startTime;
console.log(`Highlighting took ${duration.toFixed(2)}ms`);
}
}
};
hljs.addPlugin(performancePlugin);Provides Vue.js integration through a dedicated plugin system.
/**
* Returns Vue.js plugin for integration
* @returns VuePlugin object with install method
*/
function vuePlugin(): VuePlugin;
interface VuePlugin {
/** Vue.js plugin install function */
install: (vue: any) => void;
}Usage Examples:
import hljs from "highlight.js";
import Vue from "vue";
// Register with Vue 2
Vue.use(hljs.vuePlugin());
// Use in template
/*
<template>
<div>
<!-- Bind to data property -->
<highlightjs autodetect :code="codeString" />
<!-- Literal code -->
<highlightjs language='javascript' code="console.log('Hello');" />
<!-- With custom CSS classes -->
<highlightjs
language='python'
:code="pythonCode"
class="my-code-block"
/>
</div>
</template>
<script>
export default {
data() {
return {
codeString: 'def hello():\n print("Hello")',
pythonCode: 'import os\nprint(os.getcwd())'
};
}
};
</script>
*/
// Vue 3 usage
import { createApp } from 'vue';
const app = createApp({});
app.use(hljs.vuePlugin());The plugin system provides hooks at various stages of the highlighting process:
const codeTransformPlugin = {
'before:highlight': (context) => {
// Transform code before highlighting
if (context.language === 'javascript') {
// Convert arrow functions to regular functions for older browsers
context.code = context.code.replace(
/(\w+)\s*=>\s*/g,
'function($1) '
);
}
}
};
hljs.addPlugin(codeTransformPlugin);const lineNumbersPlugin = {
'after:highlightElement': (data) => {
const lines = data.text.split('\n');
const lineNumbers = lines.map((_, i) =>
`<span class="line-number">${i + 1}</span>`
).join('\n');
// Add line numbers to the element
const lineNumbersDiv = document.createElement('div');
lineNumbersDiv.className = 'line-numbers';
lineNumbersDiv.innerHTML = lineNumbers;
data.el.parentNode.insertBefore(lineNumbersDiv, data.el);
}
};
hljs.addPlugin(lineNumbersPlugin);const copyButtonPlugin = {
'after:highlightElement': (data) => {
// Add copy button to code blocks
const button = document.createElement('button');
button.textContent = 'Copy';
button.className = 'copy-button';
button.onclick = () => {
navigator.clipboard.writeText(data.text).then(() => {
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
});
};
// Add button to the parent container
const container = data.el.parentNode;
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': (data) => {
if (data.result.language) {
const badge = document.createElement('span');
badge.textContent = data.result.language;
badge.className = 'language-badge';
const container = data.el.parentNode;
if (container.tagName === 'PRE') {
container.insertBefore(badge, data.el);
}
}
}
};
hljs.addPlugin(languageBadgePlugin);class HighlightStatsPlugin {
constructor() {
this.stats = {
totalHighlights: 0,
languageCount: {},
totalTime: 0
};
}
getPlugin() {
return {
'before:highlight': (context) => {
context.pluginStartTime = performance.now();
},
'after:highlight': (result) => {
// Update statistics
this.stats.totalHighlights++;
if (result.language) {
this.stats.languageCount[result.language] =
(this.stats.languageCount[result.language] || 0) + 1;
}
if (result.context && result.context.pluginStartTime) {
this.stats.totalTime +=
performance.now() - result.context.pluginStartTime;
}
}
};
}
getStats() {
return {
...this.stats,
averageTime: this.stats.totalTime / this.stats.totalHighlights
};
}
reset() {
this.stats = {
totalHighlights: 0,
languageCount: {},
totalTime: 0
};
}
}
// Usage
const statsPlugin = new HighlightStatsPlugin();
hljs.addPlugin(statsPlugin.getPlugin());
// Later, check statistics
console.log(statsPlugin.getStats());const errorHandlingPlugin = {
'before:highlight': (context) => {
// Validate input
if (!context.code || typeof context.code !== 'string') {
console.warn('Invalid code input:', context.code);
context.code = ''; // Provide fallback
}
},
'after:highlight': (result) => {
// Check for errors
if (result.errorRaised) {
console.error('Highlighting error:', result.errorRaised);
// Could implement fallback highlighting
result.value = `<pre class="hljs-error">${result.code || ''}</pre>`;
}
// Warn about low relevance
if (result.relevance < 3) {
console.warn(`Low relevance (${result.relevance}) for language: ${result.language}`);
}
}
};
hljs.addPlugin(errorHandlingPlugin);const cspSafePlugin = {
'after:highlight': (result) => {
// Remove any inline styles that might violate CSP
result.value = result.value.replace(/style="[^"]*"/g, '');
// Log CSP violations
if (result.value.includes('javascript:') || result.value.includes('data:')) {
console.warn('Potential CSP violation in highlighted content');
}
}
};
hljs.addPlugin(cspSafePlugin);// React component with plugin system
import React, { useEffect, useRef } from 'react';
import hljs from 'highlight.js';
const CodeBlock = ({ code, language, plugins = [] }) => {
const codeRef = useRef(null);
useEffect(() => {
// Register plugins
plugins.forEach(plugin => hljs.addPlugin(plugin));
// Highlight code
if (codeRef.current) {
hljs.highlightElement(codeRef.current);
}
// Cleanup plugins if needed
return () => {
// Plugin cleanup logic
};
}, [code, language, plugins]);
return (
<pre>
<code ref={codeRef} className={`language-${language}`}>
{code}
</code>
</pre>
);
};
// Usage with custom plugins
const copyPlugin = {
'after:highlightElement': (data) => {
// Add copy functionality
}
};
<CodeBlock
code="console.log('Hello');"
language="javascript"
plugins={[copyPlugin]}
/>class PluginManager {
constructor() {
this.plugins = new Map();
this.enabled = new Set();
}
register(name, plugin, options = {}) {
if (this.plugins.has(name)) {
console.warn(`Plugin ${name} already registered, overwriting`);
}
const wrappedPlugin = this.wrapPlugin(plugin, name, options);
this.plugins.set(name, { original: plugin, wrapped: wrappedPlugin, options });
hljs.addPlugin(wrappedPlugin);
this.enabled.add(name);
return this;
}
wrapPlugin(plugin, name, options) {
const wrapped = {};
// Wrap each hook with enable/disable logic
Object.keys(plugin).forEach(hook => {
wrapped[hook] = (...args) => {
if (this.enabled.has(name)) {
try {
return plugin[hook](...args);
} catch (error) {
console.error(`Plugin ${name} error in ${hook}:`, error);
if (options.failSilently !== true) {
throw error;
}
}
}
};
});
return wrapped;
}
disable(name) {
this.enabled.delete(name);
return this;
}
enable(name) {
if (this.plugins.has(name)) {
this.enabled.add(name);
}
return this;
}
isEnabled(name) {
return this.enabled.has(name);
}
list() {
return Array.from(this.plugins.keys());
}
get(name) {
return this.plugins.get(name)?.original;
}
getStats() {
return {
total: this.plugins.size,
enabled: this.enabled.size,
disabled: this.plugins.size - this.enabled.size,
plugins: this.list().map(name => ({
name,
enabled: this.enabled.has(name),
options: this.plugins.get(name).options
}))
};
}
}
// Usage
const pluginManager = new PluginManager();
pluginManager
.register('copy-button', copyButtonPlugin)
.register('line-numbers', lineNumbersPlugin, { failSilently: true })
.register('language-badge', languageBadgePlugin);
// Temporarily disable a plugin
pluginManager.disable('line-numbers');
// Check status
console.log(pluginManager.getStats());const conditionalPlugin = {
'before:highlight': (context) => {
// Only process JavaScript files
if (context.language !== 'javascript') {
return;
}
// Only process in development mode
if (process.env.NODE_ENV === 'production') {
return;
}
console.log('Processing JavaScript code in development mode');
}
};function createConfigurablePlugin(config = {}) {
const settings = {
prefix: 'hljs-enhanced-',
logLevel: 'warn',
enableMetrics: false,
...config
};
return {
'after:highlight': (result) => {
if (settings.enableMetrics) {
console.log(`${settings.prefix}metrics: relevance=${result.relevance}`);
}
// Add custom CSS class
if (result.value) {
result.value = result.value.replace(
/<span class="hljs-/g,
`<span class="${settings.prefix}hljs-`
);
}
}
};
}
// Usage
const customPlugin = createConfigurablePlugin({
prefix: 'my-app-',
enableMetrics: true
});
hljs.addPlugin(customPlugin);class PluginChain {
constructor() {
this.beforeHighlightChain = [];
this.afterHighlightChain = [];
}
addBefore(handler) {
this.beforeHighlightChain.push(handler);
return this;
}
addAfter(handler) {
this.afterHighlightChain.push(handler);
return this;
}
getPlugin() {
return {
'before:highlight': (context) => {
this.beforeHighlightChain.forEach(handler => {
try {
handler(context);
} catch (error) {
console.error('Chain handler error:', error);
}
});
},
'after:highlight': (result) => {
this.afterHighlightChain.forEach(handler => {
try {
handler(result);
} catch (error) {
console.error('Chain handler error:', error);
}
});
}
};
}
}
// Usage
const chain = new PluginChain()
.addBefore(context => console.log('Step 1: Preprocessing'))
.addBefore(context => context.preprocessed = true)
.addAfter(result => console.log('Step 1: Postprocessing'))
.addAfter(result => result.postprocessed = true);
hljs.addPlugin(chain.getPlugin());Highlight.js includes several built-in plugins that are automatically registered and cannot be disabled:
Converts newlines to
<br>useBRFunctionality:
\n<br>hljs.configure({useBR: true})Note: This plugin is deprecated. Modern applications should handle line breaks through CSS styling instead.
Optimizes HTML output by merging adjacent identical elements to reduce DOM complexity and improve rendering performance.
Functionality:
Example:
<!-- Before merging -->
<span class="hljs-keyword">function</span><span class="hljs-keyword"> </span><span class="hljs-title">test</span>
<!-- After merging -->
<span class="hljs-keyword">function </span><span class="hljs-title">test</span>Handles tab character replacement based on the
tabReplaceFunctionality:
\thljs.configure({tabReplace: 'replacement'})Usage Example:
// Configure tab replacement
hljs.configure({tabReplace: ' '}); // Replace tabs with 4 spaces
// All highlighting operations will now replace tabs
const result = hljs.highlight('function\ttest()', {language: 'javascript'});
// Tabs in the code will be replaced with 4 spacesThese built-in plugins are automatically active and don't require manual registration. They operate at the core level and cannot be disabled through the public API.