Markdown Custom Directives and Block Extensions: Complete Guide to Advanced Content Blocks and Interactive Components
Custom Markdown directives and block extensions enable sophisticated content creation through extensible syntax systems, interactive components, and advanced formatting capabilities that extend beyond standard Markdown limitations. By implementing custom directive processors, developing reusable content blocks, and creating sophisticated extension systems, technical writers and developers can build powerful documentation platforms that combine Markdown’s simplicity with the flexibility of custom content components and interactive elements.
Why Use Custom Directives and Block Extensions?
Advanced Markdown extensibility provides essential capabilities for modern content systems:
- Enhanced Content Components: Create reusable, interactive content blocks beyond basic Markdown
- Custom Syntax Systems: Develop domain-specific markup languages for specialized content types
- Interactive Documentation: Build dynamic content with embedded functionality and user interactions
- Content Management Integration: Connect Markdown with external data sources and content management systems
- Platform Consistency: Ensure consistent styling and behavior across different content platforms
Foundation Directive Systems
Basic Directive Syntax Patterns
Common approaches to custom directive implementation:
# Standard Directive Patterns
## Block Directive Syntax
:::info
This is an information block with custom styling and behavior.
:::
:::warning title="Important Notice"
This is a warning block with a custom title parameter.
:::
:::code-example language="javascript" filename="example.js"
```javascript
function customDirective() {
console.log("Custom directive content");
}
```
::::
## Inline Directive Syntax
This paragraph contains an inline {%note%}important note{%/note%} directive.
Use {%badge color="primary"%}Custom Badge{%/badge%} for highlighting.
Insert {%icon name="warning"%} for contextual icons.
## Parameterized Directives
:::tab-group
:::tab title="JavaScript"
```javascript
// JavaScript implementation
```
:::
:::tab title="Python"
```python
# Python implementation
```
:::
:::
## Content-Rich Directives
:::definition term="API Gateway"
An API Gateway is a server that acts as an API front-end, receiving API requests, enforcing throttling and security policies, passing requests to the back-end service, and then passing the response back to the requester.
**Key Features:**
- Request routing and composition
- Authentication and authorization
- Rate limiting and throttling
- Request and response transformation
:::
:::example
**Problem**: How to implement user authentication?
**Solution**: Use JSON Web Tokens (JWT) for stateless authentication.
```javascript
const jwt = require('jsonwebtoken');
function generateToken(user) {
return jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
}
```
**Explanation**: This creates a secure token that can be verified without server-side session storage.
:::
Advanced Directive Processors
Creating sophisticated directive processing systems:
// markdown-directive-processor.js - Comprehensive directive processing system
class MarkdownDirectiveProcessor {
constructor(options = {}) {
this.options = {
allowCustomHTML: false,
validateDirectives: true,
customDirectives: new Map(),
templateEngine: 'default',
securityLevel: 'strict',
...options
};
this.directiveRegistry = new Map();
this.templateCache = new Map();
this.processingStats = {
directivesProcessed: 0,
errors: 0,
warnings: 0
};
this.initializeBuiltinDirectives();
}
initializeBuiltinDirectives() {
// Register built-in directive types
this.registerDirective('info', this.createInfoBlock.bind(this));
this.registerDirective('warning', this.createWarningBlock.bind(this));
this.registerDirective('code-example', this.createCodeExample.bind(this));
this.registerDirective('definition', this.createDefinition.bind(this));
this.registerDirective('example', this.createExample.bind(this));
this.registerDirective('tab-group', this.createTabGroup.bind(this));
this.registerDirective('tab', this.createTab.bind(this));
this.registerDirective('note', this.createInlineNote.bind(this));
this.registerDirective('badge', this.createBadge.bind(this));
this.registerDirective('icon', this.createIcon.bind(this));
}
registerDirective(name, processor) {
if (typeof processor !== 'function') {
throw new Error(`Directive processor for '${name}' must be a function`);
}
this.directiveRegistry.set(name, {
processor,
registeredAt: new Date(),
usageCount: 0
});
}
processMarkdown(content) {
try {
// Process block directives first
content = this.processBlockDirectives(content);
// Then process inline directives
content = this.processInlineDirectives(content);
// Post-process for nested directives
content = this.processNestedDirectives(content);
return content;
} catch (error) {
this.processingStats.errors++;
console.error('Directive processing error:', error);
return content; // Return original content on error
}
}
processBlockDirectives(content) {
// Pattern for block directives: :::directive-name params\ncontent\n:::
const blockDirectivePattern = /:::(\w+)(?:\s+([^\n]*))?\n([\s\S]*?)\n:::/g;
return content.replace(blockDirectivePattern, (match, directiveName, params, directiveContent) => {
try {
this.processingStats.directivesProcessed++;
const directive = this.directiveRegistry.get(directiveName);
if (!directive) {
this.processingStats.warnings++;
console.warn(`Unknown directive: ${directiveName}`);
return this.createErrorBlock(`Unknown directive: ${directiveName}`);
}
directive.usageCount++;
const parsedParams = this.parseDirectiveParams(params);
const context = {
name: directiveName,
params: parsedParams,
content: directiveContent.trim(),
type: 'block'
};
return directive.processor(context);
} catch (error) {
this.processingStats.errors++;
console.error(`Error processing directive ${directiveName}:`, error);
return this.createErrorBlock(`Error in directive: ${directiveName}`);
}
});
}
processInlineDirectives(content) {
// Pattern for inline directives: {%directive-name params%}content{%/directive-name%}
const inlineDirectivePattern = /\{%(\w+)(?:\s+([^%]*))?\%\}(.*?)\{%\/\1\%\}/g;
return content.replace(inlineDirectivePattern, (match, directiveName, params, directiveContent) => {
try {
this.processingStats.directivesProcessed++;
const directive = this.directiveRegistry.get(directiveName);
if (!directive) {
this.processingStats.warnings++;
console.warn(`Unknown inline directive: ${directiveName}`);
return `<span class="directive-error">Unknown directive: ${directiveName}</span>`;
}
directive.usageCount++;
const parsedParams = this.parseDirectiveParams(params);
const context = {
name: directiveName,
params: parsedParams,
content: directiveContent.trim(),
type: 'inline'
};
return directive.processor(context);
} catch (error) {
this.processingStats.errors++;
console.error(`Error processing inline directive ${directiveName}:`, error);
return `<span class="directive-error">Error in directive: ${directiveName}</span>`;
}
});
}
processNestedDirectives(content) {
// Handle special cases like tab-group containers
return content.replace(/:::tab-group\n([\s\S]*?)\n:::/g, (match, tabsContent) => {
const tabs = [];
const tabPattern = /:::tab\s+title="([^"]+)"(?:\s+([^\n]*))?\n([\s\S]*?)(?=:::tab|$)/g;
let tabMatch;
while ((tabMatch = tabPattern.exec(tabsContent)) !== null) {
tabs.push({
title: tabMatch[1],
params: this.parseDirectiveParams(tabMatch[2] || ''),
content: tabMatch[3].trim()
});
}
return this.createTabGroup({ content: '', params: {}, tabs });
});
}
parseDirectiveParams(paramString) {
if (!paramString) return {};
const params = {};
// Handle quoted parameters: key="value"
const quotedParamPattern = /(\w+)="([^"]*)"/g;
let match;
while ((match = quotedParamPattern.exec(paramString)) !== null) {
params[match[1]] = match[2];
}
// Handle unquoted parameters: key=value
const unquotedParamPattern = /(\w+)=(\w+)/g;
const cleanedString = paramString.replace(quotedParamPattern, '');
while ((match = unquotedParamPattern.exec(cleanedString)) !== null) {
params[match[1]] = match[2];
}
// Handle boolean flags: just the parameter name
const remainingString = cleanedString.replace(unquotedParamPattern, '').trim();
if (remainingString) {
remainingString.split(/\s+/).forEach(flag => {
if (flag && !params[flag]) {
params[flag] = true;
}
});
}
return params;
}
// Built-in directive implementations
createInfoBlock(context) {
const { content, params } = context;
const title = params.title || '';
const icon = params.icon || 'ℹ️';
return `
<div class="directive-block directive-info">
${title ? `<div class="directive-header">
<span class="directive-icon">${icon}</span>
<span class="directive-title">${this.escapeHtml(title)}</span>
</div>` : ''}
<div class="directive-content">
${this.processInnerContent(content)}
</div>
</div>`;
}
createWarningBlock(context) {
const { content, params } = context;
const title = params.title || 'Warning';
const icon = params.icon || '⚠️';
return `
<div class="directive-block directive-warning">
<div class="directive-header">
<span class="directive-icon">${icon}</span>
<span class="directive-title">${this.escapeHtml(title)}</span>
</div>
<div class="directive-content">
${this.processInnerContent(content)}
</div>
</div>`;
}
createCodeExample(context) {
const { content, params } = context;
const language = params.language || 'text';
const filename = params.filename || '';
const highlight = params.highlight || '';
return `
<div class="directive-block directive-code-example">
${filename ? `<div class="code-filename">${this.escapeHtml(filename)}</div>` : ''}
<div class="code-container">
<pre class="code-block language-${language}"${highlight ? ` data-highlight="${highlight}"` : ''}><code>${this.escapeHtml(content)}</code></pre>
</div>
</div>`;
}
createDefinition(context) {
const { content, params } = context;
const term = params.term || '';
return `
<div class="directive-block directive-definition">
<div class="definition-term">${this.escapeHtml(term)}</div>
<div class="definition-content">
${this.processInnerContent(content)}
</div>
</div>`;
}
createExample(context) {
const { content, params } = context;
const title = params.title || 'Example';
return `
<div class="directive-block directive-example">
<div class="directive-header">
<span class="directive-icon">💡</span>
<span class="directive-title">${this.escapeHtml(title)}</span>
</div>
<div class="directive-content">
${this.processInnerContent(content)}
</div>
</div>`;
}
createTabGroup(context) {
const { tabs } = context;
if (!tabs || tabs.length === 0) return '';
const tabsId = this.generateId();
let tabsHtml = `<div class="directive-tabs" data-tabs-id="${tabsId}">`;
// Create tab headers
tabsHtml += '<div class="tab-headers">';
tabs.forEach((tab, index) => {
const activeClass = index === 0 ? ' active' : '';
tabsHtml += `<button class="tab-header${activeClass}" data-tab="${index}">${this.escapeHtml(tab.title)}</button>`;
});
tabsHtml += '</div>';
// Create tab content
tabsHtml += '<div class="tab-contents">';
tabs.forEach((tab, index) => {
const activeClass = index === 0 ? ' active' : '';
tabsHtml += `<div class="tab-content${activeClass}" data-tab="${index}">
${this.processInnerContent(tab.content)}
</div>`;
});
tabsHtml += '</div>';
tabsHtml += '</div>';
return tabsHtml;
}
createTab(context) {
// This is handled by createTabGroup, but we need the method for registration
return '';
}
createInlineNote(context) {
const { content, params } = context;
const type = params.type || 'info';
return `<span class="directive-inline directive-note directive-note-${type}">${this.escapeHtml(content)}</span>`;
}
createBadge(context) {
const { content, params } = context;
const color = params.color || 'primary';
const size = params.size || 'normal';
return `<span class="directive-badge badge-${color} badge-${size}">${this.escapeHtml(content)}</span>`;
}
createIcon(context) {
const { params } = context;
const name = params.name || '';
const size = params.size || '';
if (!name) return '';
return `<span class="directive-icon icon-${name}${size ? ` icon-${size}` : ''}" aria-hidden="true"></span>`;
}
createErrorBlock(message) {
return `
<div class="directive-error-block">
<strong>Directive Error:</strong> ${this.escapeHtml(message)}
</div>`;
}
processInnerContent(content) {
// Process Markdown within directive content
if (typeof this.options.markdownProcessor === 'function') {
return this.options.markdownProcessor(content);
}
// Basic markdown processing for common cases
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\n\n/g, '</p><p>')
.replace(/^(?!<p>)/, '<p>')
.replace(/(?<!<\/p>)$/, '</p>');
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
generateId() {
return Math.random().toString(36).substr(2, 9);
}
getStats() {
return {
...this.processingStats,
registeredDirectives: Array.from(this.directiveRegistry.keys()),
directiveUsage: Array.from(this.directiveRegistry.entries()).map(([name, data]) => ({
name,
usageCount: data.usageCount
}))
};
}
}
// Initialize directive processor
const directiveProcessor = new MarkdownDirectiveProcessor();
// CSS for directive styling
const directiveStyles = `
/* Directive Block Styles */
.directive-block {
margin: 1.5rem 0;
border-radius: 0.375rem;
overflow: hidden;
}
.directive-info {
background: #e3f2fd;
border-left: 4px solid #2196f3;
}
.directive-warning {
background: #fff3cd;
border-left: 4px solid #ff9800;
}
.directive-example {
background: #f0f9ff;
border-left: 4px solid #0ea5e9;
}
.directive-definition {
background: #f8f9fa;
border: 1px solid #dee2e6;
}
.directive-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(0,0,0,0.05);
font-weight: 600;
}
.directive-icon {
margin-right: 0.5rem;
font-size: 1.1em;
}
.directive-content {
padding: 1rem;
}
.directive-content p:last-child {
margin-bottom: 0;
}
/* Code Example Styles */
.directive-code-example {
background: #f8f9fa;
border: 1px solid #dee2e6;
}
.code-filename {
background: #495057;
color: white;
padding: 0.5rem 1rem;
font-family: monospace;
font-size: 0.9rem;
}
.code-container {
overflow-x: auto;
}
.code-block {
margin: 0;
padding: 1rem;
background: #f8f9fa;
border: none;
font-family: 'Courier New', monospace;
line-height: 1.5;
}
/* Tab Styles */
.directive-tabs {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
overflow: hidden;
}
.tab-headers {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.tab-header {
background: none;
border: none;
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}
.tab-header:hover {
background: #e9ecef;
}
.tab-header.active {
background: white;
border-bottom-color: #007cba;
color: #007cba;
font-weight: 600;
}
.tab-contents {
position: relative;
}
.tab-content {
display: none;
padding: 1rem;
}
.tab-content.active {
display: block;
}
/* Inline Directive Styles */
.directive-inline {
display: inline;
}
.directive-note {
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.9rem;
}
.directive-note-info {
background: #cfe2ff;
color: #084298;
}
.directive-note-warning {
background: #fff3cd;
color: #664d03;
}
.directive-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.375rem;
}
.badge-primary {
color: white;
background-color: #007cba;
}
.badge-success {
color: white;
background-color: #28a745;
}
.badge-warning {
color: #212529;
background-color: #ffc107;
}
.badge-danger {
color: white;
background-color: #dc3545;
}
.badge-small {
padding: 0.125rem 0.375rem;
font-size: 0.75rem;
}
.badge-large {
padding: 0.375rem 0.75rem;
font-size: 1rem;
}
/* Definition Styles */
.definition-term {
font-weight: 700;
font-size: 1.1em;
color: #495057;
margin-bottom: 0.5rem;
}
.definition-content {
color: #6c757d;
}
/* Error Styles */
.directive-error-block {
background: #f8d7da;
color: #721c24;
padding: 1rem;
border: 1px solid #f5c6cb;
border-radius: 0.375rem;
margin: 1rem 0;
}
.directive-error {
color: #dc3545;
font-style: italic;
}
/* Responsive Design */
@media (max-width: 768px) {
.directive-block {
margin: 1rem 0;
}
.directive-header,
.directive-content {
padding: 0.75rem;
}
.tab-headers {
flex-wrap: wrap;
}
.tab-header {
min-width: 0;
flex: 1;
}
}
`;
// JavaScript for interactive functionality
const directiveInteractions = `
// Tab functionality
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.directive-tabs').forEach(tabGroup => {
const headers = tabGroup.querySelectorAll('.tab-header');
const contents = tabGroup.querySelectorAll('.tab-content');
headers.forEach(header => {
header.addEventListener('click', () => {
const tabIndex = header.getAttribute('data-tab');
// Remove active class from all headers and contents
headers.forEach(h => h.classList.remove('active'));
contents.forEach(c => c.classList.remove('active'));
// Add active class to clicked header and corresponding content
header.classList.add('active');
const activeContent = tabGroup.querySelector(\`[data-tab="\${tabIndex}"].tab-content\`);
if (activeContent) {
activeContent.classList.add('active');
}
});
});
});
});
`;
// Inject styles and scripts
if (typeof document !== 'undefined') {
const style = document.createElement('style');
style.textContent = directiveStyles;
document.head.appendChild(style);
const script = document.createElement('script');
script.textContent = directiveInteractions;
document.head.appendChild(script);
}
Advanced Extension Systems
Plugin Architecture for Directives
Create modular, extensible directive systems:
// markdown-directive-plugin-system.js
class MarkdownDirectivePluginSystem {
constructor() {
this.plugins = new Map();
this.hooks = {
beforeProcessing: [],
afterProcessing: [],
onDirectiveRegistered: [],
onDirectiveProcessed: [],
onError: []
};
this.processor = null;
this.config = {
debug: false,
strictMode: true,
allowDynamicRegistration: true
};
}
registerPlugin(name, plugin) {
if (this.plugins.has(name)) {
throw new Error(`Plugin '${name}' is already registered`);
}
// Validate plugin structure
this.validatePlugin(plugin);
// Initialize plugin
if (typeof plugin.init === 'function') {
plugin.init(this);
}
// Register plugin directives
if (plugin.directives) {
Object.entries(plugin.directives).forEach(([directiveName, processor]) => {
this.registerDirective(`${name}:${directiveName}`, processor);
});
}
// Register plugin hooks
if (plugin.hooks) {
Object.entries(plugin.hooks).forEach(([hookName, handler]) => {
this.addHook(hookName, handler);
});
}
this.plugins.set(name, plugin);
this.emit('onDirectiveRegistered', { name, plugin });
}
validatePlugin(plugin) {
if (typeof plugin !== 'object' || plugin === null) {
throw new Error('Plugin must be an object');
}
if (!plugin.name || typeof plugin.name !== 'string') {
throw new Error('Plugin must have a name property');
}
if (!plugin.version || typeof plugin.version !== 'string') {
throw new Error('Plugin must have a version property');
}
if (plugin.directives && typeof plugin.directives !== 'object') {
throw new Error('Plugin directives must be an object');
}
}
registerDirective(name, processor) {
if (!this.processor) {
this.processor = new MarkdownDirectiveProcessor();
}
this.processor.registerDirective(name, processor);
}
addHook(name, handler) {
if (!this.hooks[name]) {
this.hooks[name] = [];
}
this.hooks[name].push(handler);
}
emit(hookName, data) {
if (this.hooks[hookName]) {
this.hooks[hookName].forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(`Error in hook ${hookName}:`, error);
}
});
}
}
processMarkdown(content) {
try {
this.emit('beforeProcessing', { content });
if (!this.processor) {
this.processor = new MarkdownDirectiveProcessor();
}
const result = this.processor.processMarkdown(content);
this.emit('afterProcessing', { content, result });
return result;
} catch (error) {
this.emit('onError', { error, content });
throw error;
}
}
getPlugin(name) {
return this.plugins.get(name);
}
listPlugins() {
return Array.from(this.plugins.keys());
}
unregisterPlugin(name) {
const plugin = this.plugins.get(name);
if (plugin) {
// Call plugin cleanup if available
if (typeof plugin.cleanup === 'function') {
plugin.cleanup(this);
}
// Remove plugin directives
if (plugin.directives) {
Object.keys(plugin.directives).forEach(directiveName => {
// Remove from processor if available
if (this.processor) {
this.processor.directiveRegistry.delete(`${name}:${directiveName}`);
}
});
}
this.plugins.delete(name);
return true;
}
return false;
}
}
// Example plugins
const chartPlugin = {
name: 'charts',
version: '1.0.0',
description: 'Adds chart generation directives',
directives: {
'bar-chart': function(context) {
const { content, params } = context;
const data = this.parseChartData(content);
const chartId = this.generateId();
return `
<div class="chart-container">
<canvas id="${chartId}" data-chart-type="bar" data-chart-data='${JSON.stringify(data)}' data-chart-options='${JSON.stringify(params)}'></canvas>
</div>`;
},
'line-chart': function(context) {
const { content, params } = context;
const data = this.parseChartData(content);
const chartId = this.generateId();
return `
<div class="chart-container">
<canvas id="${chartId}" data-chart-type="line" data-chart-data='${JSON.stringify(data)}' data-chart-options='${JSON.stringify(params)}'></canvas>
</div>`;
},
'pie-chart': function(context) {
const { content, params } = context;
const data = this.parseChartData(content);
const chartId = this.generateId();
return `
<div class="chart-container">
<canvas id="${chartId}" data-chart-type="pie" data-chart-data='${JSON.stringify(data)}' data-chart-options='${JSON.stringify(params)}'></canvas>
</div>`;
}
},
parseChartData: function(content) {
// Parse CSV-like data from content
const lines = content.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const data = lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
const row = {};
headers.forEach((header, index) => {
row[header] = isNaN(values[index]) ? values[index] : parseFloat(values[index]);
});
return row;
});
return {
headers,
data
};
},
generateId: function() {
return 'chart-' + Math.random().toString(36).substr(2, 9);
},
init: function(system) {
console.log('Chart plugin initialized');
// Load Chart.js if not already loaded
if (typeof Chart === 'undefined') {
this.loadChartJS();
}
},
loadChartJS: function() {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
script.onload = () => {
// Initialize charts after Chart.js loads
this.initializeCharts();
};
document.head.appendChild(script);
},
initializeCharts: function() {
document.querySelectorAll('[data-chart-type]').forEach(canvas => {
const chartType = canvas.getAttribute('data-chart-type');
const chartData = JSON.parse(canvas.getAttribute('data-chart-data'));
const chartOptions = JSON.parse(canvas.getAttribute('data-chart-options') || '{}');
this.renderChart(canvas, chartType, chartData, chartOptions);
});
},
renderChart: function(canvas, type, data, options) {
const ctx = canvas.getContext('2d');
// Convert data format for Chart.js
const chartData = {
labels: data.data.map(row => row[data.headers[0]]),
datasets: [{
label: data.headers[1] || 'Data',
data: data.data.map(row => row[data.headers[1]]),
backgroundColor: this.generateColors(data.data.length),
borderColor: options.borderColor || '#007cba',
borderWidth: options.borderWidth || 1
}]
};
new Chart(ctx, {
type: type,
data: chartData,
options: {
responsive: true,
plugins: {
title: {
display: !!options.title,
text: options.title || ''
}
},
...options
}
});
},
generateColors: function(count) {
const colors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0',
'#9966FF', '#FF9F40', '#FF6384', '#C9CBCF'
];
return Array.from({length: count}, (_, i) => colors[i % colors.length]);
}
};
const interactivePlugin = {
name: 'interactive',
version: '1.0.0',
description: 'Adds interactive content directives',
directives: {
'accordion': function(context) {
const { content, params } = context;
const accordionId = this.generateId();
const items = this.parseAccordionItems(content);
let html = `<div class="accordion" id="${accordionId}">`;
items.forEach((item, index) => {
const itemId = `${accordionId}-item-${index}`;
const expanded = params.expanded && params.expanded.includes(index.toString());
html += `
<div class="accordion-item">
<div class="accordion-header">
<button class="accordion-button${expanded ? '' : ' collapsed'}"
type="button"
data-toggle="collapse"
data-target="#${itemId}"
aria-expanded="${expanded}"
aria-controls="${itemId}">
${item.title}
</button>
</div>
<div id="${itemId}" class="accordion-collapse collapse${expanded ? ' show' : ''}" data-parent="#${accordionId}">
<div class="accordion-body">
${item.content}
</div>
</div>
</div>`;
});
html += '</div>';
return html;
},
'collapsible': function(context) {
const { content, params } = context;
const collapsibleId = this.generateId();
const title = params.title || 'Click to expand';
const expanded = params.expanded === 'true';
return `
<div class="collapsible">
<button class="collapsible-toggle${expanded ? ' expanded' : ''}"
data-target="#${collapsibleId}"
aria-expanded="${expanded}">
<span class="toggle-icon">${expanded ? '▼' : '▶'}</span>
${title}
</button>
<div id="${collapsibleId}" class="collapsible-content${expanded ? ' show' : ''}">
<div class="collapsible-body">
${content}
</div>
</div>
</div>`;
},
'modal': function(context) {
const { content, params } = context;
const modalId = this.generateId();
const title = params.title || '';
const triggerText = params.trigger || 'Open Modal';
return `
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#${modalId}">
${triggerText}
</button>
<div class="modal fade" id="${modalId}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
${title ? `<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button type="button" class="btn-close" data-dismiss="modal"></button>
</div>` : ''}
<div class="modal-body">
${content}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>`;
}
},
parseAccordionItems: function(content) {
const sections = content.split(/^## /m).filter(section => section.trim());
return sections.map(section => {
const lines = section.trim().split('\n');
const title = lines[0];
const content = lines.slice(1).join('\n').trim();
return { title, content };
});
},
generateId: function() {
return 'interactive-' + Math.random().toString(36).substr(2, 9);
},
init: function(system) {
console.log('Interactive plugin initialized');
this.addEventListeners();
},
addEventListeners: function() {
document.addEventListener('click', (e) => {
// Handle collapsible toggles
if (e.target.matches('.collapsible-toggle')) {
this.toggleCollapsible(e.target);
}
// Handle accordion buttons
if (e.target.matches('.accordion-button')) {
this.toggleAccordion(e.target);
}
});
},
toggleCollapsible: function(button) {
const targetId = button.getAttribute('data-target');
const content = document.querySelector(targetId);
const icon = button.querySelector('.toggle-icon');
if (content.classList.contains('show')) {
content.classList.remove('show');
button.classList.remove('expanded');
button.setAttribute('aria-expanded', 'false');
icon.textContent = '▶';
} else {
content.classList.add('show');
button.classList.add('expanded');
button.setAttribute('aria-expanded', 'true');
icon.textContent = '▼';
}
},
toggleAccordion: function(button) {
const targetId = button.getAttribute('data-target');
const content = document.querySelector(targetId);
const accordion = button.closest('.accordion');
// Close other items in the same accordion
accordion.querySelectorAll('.accordion-collapse.show').forEach(item => {
if (item !== content) {
item.classList.remove('show');
const otherButton = accordion.querySelector(`[data-target="#${item.id}"]`);
if (otherButton) {
otherButton.classList.add('collapsed');
otherButton.setAttribute('aria-expanded', 'false');
}
}
});
// Toggle current item
if (content.classList.contains('show')) {
content.classList.remove('show');
button.classList.add('collapsed');
button.setAttribute('aria-expanded', 'false');
} else {
content.classList.add('show');
button.classList.remove('collapsed');
button.setAttribute('aria-expanded', 'true');
}
}
};
// Initialize plugin system
const pluginSystem = new MarkdownDirectivePluginSystem();
// Register plugins
pluginSystem.registerPlugin('charts', chartPlugin);
pluginSystem.registerPlugin('interactive', interactivePlugin);
// Export for use
window.markdownPluginSystem = pluginSystem;
Static Site Generator Integration
Jekyll Liquid Extension
Create Jekyll-compatible directive processing:
<!-- _plugins/directive_processor.rb -->
require 'liquid'
module Jekyll
class DirectiveProcessor < Liquid::Block
def initialize(tag_name, params, tokens)
super
@directive_name = tag_name
@params = parse_params(params)
end
def render(context)
content = super
case @directive_name
when 'info'
render_info_block(content, @params)
when 'warning'
render_warning_block(content, @params)
when 'code_example'
render_code_example(content, @params)
when 'definition'
render_definition(content, @params)
else
content
end
end
private
def parse_params(param_string)
params = {}
return params if param_string.nil?
# Parse key="value" pairs
param_string.scan(/(\w+)="([^"]*)"/) do |key, value|
params[key] = value
end
# Parse key=value pairs
param_string.scan(/(\w+)=(\w+)/) do |key, value|
params[key] = value unless params.key?(key)
end
params
end
def render_info_block(content, params)
title = params['title'] || ''
icon = params['icon'] || 'ℹ️'
<<~HTML
<div class="directive-block directive-info">
#{title.empty? ? '' : %(<div class="directive-header">
<span class="directive-icon">#{icon}</span>
<span class="directive-title">#{title}</span>
</div>)}
<div class="directive-content">
#{markdownify(content)}
</div>
</div>
HTML
end
def render_warning_block(content, params)
title = params['title'] || 'Warning'
icon = params['icon'] || '⚠️'
<<~HTML
<div class="directive-block directive-warning">
<div class="directive-header">
<span class="directive-icon">#{icon}</span>
<span class="directive-title">#{title}</span>
</div>
<div class="directive-content">
#{markdownify(content)}
</div>
</div>
HTML
end
def render_code_example(content, params)
language = params['language'] || 'text'
filename = params['filename'] || ''
<<~HTML
<div class="directive-block directive-code-example">
#{filename.empty? ? '' : %(<div class="code-filename">#{filename}</div>)}
<div class="code-container">
<pre class="code-block language-#{language}"><code>#{content.strip}</code></pre>
</div>
</div>
HTML
end
def render_definition(content, params)
term = params['term'] || ''
<<~HTML
<div class="directive-block directive-definition">
<div class="definition-term">#{term}</div>
<div class="definition-content">
#{markdownify(content)}
</div>
</div>
HTML
end
def markdownify(content)
Jekyll::Converters::Markdown.new({}).convert(content)
end
end
end
# Register directive tags
Liquid::Template.register_tag('info', Jekyll::DirectiveProcessor)
Liquid::Template.register_tag('warning', Jekyll::DirectiveProcessor)
Liquid::Template.register_tag('code_example', Jekyll::DirectiveProcessor)
Liquid::Template.register_tag('definition', Jekyll::DirectiveProcessor)
<!-- Usage in Markdown files: -->
{% info title="Important Information" %}
This is important information that readers should notice.
It supports **markdown formatting** and `inline code`.
{% endinfo %}
{% warning title="Critical Warning" %}
This is a critical warning with important safety information.
{% endwarning %}
{% code_example language="javascript" filename="example.js" %}
function exampleFunction() {
console.log("This is an example");
}
{% endcode_example %}
{% definition term="API Endpoint" %}
An API endpoint is a specific URL where an API can be accessed by a client application.
{% enddefinition %}
Hugo Shortcode System
Advanced shortcode implementations for Hugo:
// layouts/shortcodes/directive.html
{{ $type := .Get "type" | default "info" }}
{{ $title := .Get "title" }}
{{ $icon := .Get "icon" }}
<div class="directive-block directive-{{ $type }}">
{{ if $title }}
<div class="directive-header">
{{ if $icon }}
<span class="directive-icon">{{ $icon }}</span>
{{ else }}
{{ if eq $type "info" }}<span class="directive-icon">ℹ️</span>{{ end }}
{{ if eq $type "warning" }}<span class="directive-icon">⚠️</span>{{ end }}
{{ if eq $type "example" }}<span class="directive-icon">💡</span>{{ end }}
{{ end }}
<span class="directive-title">{{ $title }}</span>
</div>
{{ end }}
<div class="directive-content">
{{ .Inner | markdownify }}
</div>
</div>
// layouts/shortcodes/tabs.html
{{ $tabsId := printf "tabs-%d" (rand 1000) }}
<div class="directive-tabs" data-tabs-id="{{ $tabsId }}">
<div class="tab-headers">
{{ range $index, $tab := .Params.tabs }}
<button class="tab-header{{ if eq $index 0 }} active{{ end }}" data-tab="{{ $index }}">
{{ $tab.title }}
</button>
{{ end }}
</div>
<div class="tab-contents">
{{ range $index, $tab := .Params.tabs }}
<div class="tab-content{{ if eq $index 0 }} active{{ end }}" data-tab="{{ $index }}">
{{ $tab.content | markdownify }}
</div>
{{ end }}
</div>
</div>
Usage in Hugo Markdown:
{{< directive type="info" title="Getting Started" >}}
This is an information block with **markdown** support.
{{< /directive >}}
{{< directive type="warning" title="Important Notice" >}}
This is a warning with critical information.
{{< /directive >}}
{{< tabs >}}
{{< tab title="JavaScript" >}}
```javascript
console.log("Hello World");
{{< /tab >}}
{{< tab title=”Python” >}}
print("Hello World")
{{< /tab >}}
{{< /tabs >}}
## Security and Performance Considerations
### Secure Directive Processing
Implement security measures for custom directives:
```javascript
// secure-directive-processor.js - Security-focused directive processing
class SecureDirectiveProcessor {
constructor(options = {}) {
this.securityConfig = {
allowedTags: ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'code', 'pre', 'strong', 'em'],
allowedAttributes: ['class', 'id', 'data-*', 'aria-*'],
allowedProtocols: ['http:', 'https:', 'mailto:'],
maxDirectiveDepth: 5,
maxContentLength: 50000,
sanitizeHTML: true,
...options.security
};
this.performanceConfig = {
maxDirectivesPerDocument: 500,
processingTimeout: 5000,
enableCaching: true,
cacheSize: 1000,
...options.performance
};
this.cache = new Map();
this.stats = {
processed: 0,
cached: 0,
errors: 0,
securityViolations: 0
};
}
processContent(content) {
try {
// Pre-processing security checks
if (!this.validateContent(content)) {
throw new Error('Content validation failed');
}
// Check cache first
const cacheKey = this.generateCacheKey(content);
if (this.performanceConfig.enableCaching && this.cache.has(cacheKey)) {
this.stats.cached++;
return this.cache.get(cacheKey);
}
// Process with timeout
const result = this.processWithTimeout(content);
// Cache result
if (this.performanceConfig.enableCaching) {
this.cacheResult(cacheKey, result);
}
this.stats.processed++;
return result;
} catch (error) {
this.stats.errors++;
console.error('Directive processing error:', error);
return this.createErrorResponse(error);
}
}
validateContent(content) {
// Length validation
if (content.length > this.securityConfig.maxContentLength) {
this.stats.securityViolations++;
return false;
}
// Check for suspicious patterns
const suspiciousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/on\w+\s*=/gi,
/data:(?!image\/[png|jpg|jpeg|gif|svg])/gi
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(content)) {
this.stats.securityViolations++;
return false;
}
}
return true;
}
processWithTimeout(content) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Processing timeout exceeded'));
}, this.performanceConfig.processingTimeout);
try {
const result = this.processDirectives(content);
clearTimeout(timeout);
resolve(result);
} catch (error) {
clearTimeout(timeout);
reject(error);
}
});
}
processDirectives(content) {
// Count directives to prevent abuse
const directiveCount = (content.match(/:::\w+/g) || []).length;
if (directiveCount > this.performanceConfig.maxDirectivesPerDocument) {
throw new Error('Too many directives in document');
}
// Process with depth tracking
return this.processWithDepthTracking(content, 0);
}
processWithDepthTracking(content, depth) {
if (depth > this.securityConfig.maxDirectiveDepth) {
throw new Error('Maximum directive nesting depth exceeded');
}
// Process directives at current level
const directivePattern = /:::(\w+)(?:\s+([^\n]*))?\n([\s\S]*?)\n:::/g;
return content.replace(directivePattern, (match, type, params, innerContent) => {
// Validate directive type
if (!this.isAllowedDirective(type)) {
this.stats.securityViolations++;
return this.createSecurityError(`Directive type '${type}' not allowed`);
}
// Sanitize parameters
const sanitizedParams = this.sanitizeParameters(params);
// Process nested content
const processedContent = this.processWithDepthTracking(innerContent, depth + 1);
// Generate directive HTML
const html = this.generateDirectiveHTML(type, sanitizedParams, processedContent);
// Sanitize output HTML
return this.sanitizeHTML(html);
});
}
isAllowedDirective(type) {
const allowedDirectives = [
'info', 'warning', 'example', 'definition',
'code-example', 'tab-group', 'tab'
];
return allowedDirectives.includes(type);
}
sanitizeParameters(paramString) {
if (!paramString) return {};
const params = {};
const paramPattern = /(\w+)="([^"]*)"/g;
let match;
while ((match = paramPattern.exec(paramString)) !== null) {
const key = match[1];
const value = match[2];
// Validate parameter name
if (!/^\w+$/.test(key)) {
continue;
}
// Sanitize parameter value
params[key] = this.sanitizeParameterValue(value);
}
return params;
}
sanitizeParameterValue(value) {
// Remove potentially dangerous content
return value
.replace(/[<>'"]/g, '')
.replace(/javascript:/gi, '')
.replace(/on\w+/gi, '')
.substring(0, 200); // Limit length
}
sanitizeHTML(html) {
if (!this.securityConfig.sanitizeHTML) {
return html;
}
// Basic HTML sanitization
const div = document.createElement('div');
div.innerHTML = html;
// Remove disallowed tags
this.removeDisallowedElements(div);
// Sanitize attributes
this.sanitizeAttributes(div);
return div.innerHTML;
}
removeDisallowedElements(container) {
const elements = container.querySelectorAll('*');
elements.forEach(element => {
if (!this.securityConfig.allowedTags.includes(element.tagName.toLowerCase())) {
element.remove();
}
});
}
sanitizeAttributes(container) {
const elements = container.querySelectorAll('*');
elements.forEach(element => {
const attributesToRemove = [];
for (const attr of element.attributes) {
if (!this.isAllowedAttribute(attr.name)) {
attributesToRemove.push(attr.name);
} else if (attr.name === 'href' || attr.name === 'src') {
if (!this.isAllowedURL(attr.value)) {
attributesToRemove.push(attr.name);
}
}
}
attributesToRemove.forEach(attrName => {
element.removeAttribute(attrName);
});
});
}
isAllowedAttribute(attrName) {
return this.securityConfig.allowedAttributes.some(allowed => {
if (allowed.endsWith('*')) {
return attrName.startsWith(allowed.slice(0, -1));
}
return attrName === allowed;
});
}
isAllowedURL(url) {
try {
const parsed = new URL(url, window.location.href);
return this.securityConfig.allowedProtocols.includes(parsed.protocol);
} catch {
return false;
}
}
generateDirectiveHTML(type, params, content) {
// Use safe HTML generation methods
switch (type) {
case 'info':
return this.generateInfoDirective(params, content);
case 'warning':
return this.generateWarningDirective(params, content);
default:
return content;
}
}
generateInfoDirective(params, content) {
const title = params.title || '';
return `
<div class="directive-block directive-info">
${title ? `<div class="directive-header">
<span class="directive-title">${this.escapeHTML(title)}</span>
</div>` : ''}
<div class="directive-content">${content}</div>
</div>`;
}
generateWarningDirective(params, content) {
const title = params.title || 'Warning';
return `
<div class="directive-block directive-warning">
<div class="directive-header">
<span class="directive-title">${this.escapeHTML(title)}</span>
</div>
<div class="directive-content">${content}</div>
</div>`;
}
escapeHTML(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
generateCacheKey(content) {
// Simple hash function for cache keys
let hash = 0;
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString();
}
cacheResult(key, result) {
if (this.cache.size >= this.performanceConfig.cacheSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, result);
}
createErrorResponse(error) {
return `<div class="directive-error">Error processing directives: ${this.escapeHTML(error.message)}</div>`;
}
createSecurityError(message) {
return `<div class="directive-security-error">Security violation: ${this.escapeHTML(message)}</div>`;
}
getStats() {
return {
...this.stats,
cacheSize: this.cache.size,
cacheHitRate: this.stats.cached / (this.stats.processed + this.stats.cached)
};
}
clearCache() {
this.cache.clear();
}
}
// Initialize secure processor
const secureProcessor = new SecureDirectiveProcessor({
security: {
allowedTags: ['div', 'span', 'p', 'code', 'pre', 'strong', 'em', 'h1', 'h2', 'h3', 'h4'],
allowedAttributes: ['class', 'id', 'data-*', 'aria-*'],
maxDirectiveDepth: 3,
maxContentLength: 100000
},
performance: {
maxDirectivesPerDocument: 200,
processingTimeout: 3000,
enableCaching: true
}
});
Integration with Modern Documentation Systems
Custom directive systems integrate seamlessly with comprehensive content management platforms. When combined with automation workflows and content processing, custom directives enable sophisticated build processes that can validate content, generate interactive components, and maintain consistency across large documentation projects.
For advanced content architectures, directive systems work effectively with Progressive Web App documentation platforms to provide offline functionality, cached interactive components, and enhanced user experiences for technical documentation systems.
When building comprehensive documentation ecosystems, custom directives complement advanced table management and data presentation systems by enabling contextual content blocks, interactive data visualizations, and sophisticated information organization patterns.
Conclusion
Custom Markdown directives and block extensions transform static content into dynamic, interactive documentation platforms that maintain Markdown’s simplicity while providing powerful extensibility. By implementing systematic directive processors, creating reusable content components, and establishing secure extension architectures, technical teams can build sophisticated content management systems that scale with project requirements while maintaining consistency and performance.
The key to successful directive implementation lies in balancing functionality with security, establishing clear syntax conventions, and creating maintainable processing systems that can evolve with changing requirements. Whether you’re building technical documentation, educational content, or interactive guides, the directive techniques covered in this guide provide the foundation for creating rich, engaging content experiences.
Remember to prioritize security in directive processing, implement comprehensive error handling, and establish clear guidelines for directive usage within your team. With proper implementation of custom Markdown directive systems, your documentation can achieve new levels of interactivity and user engagement while preserving the clean, readable format that makes Markdown an effective content creation tool.