Markdown Custom Directives and Extensions: Complete Guide for Advanced Content Management and Interactive Features
Markdown custom directives and extensions enable advanced content management by extending standard Markdown syntax with specialized features, interactive components, and domain-specific functionality. While basic Markdown provides excellent foundational formatting capabilities, custom directives allow technical teams to create sophisticated content systems with reusable components, conditional rendering, and dynamic content generation tailored to specific organizational needs and workflow requirements.
Why Master Markdown Custom Directives?
Professional custom directive implementation provides essential benefits for advanced content systems:
- Enhanced Functionality: Extend Markdown beyond basic formatting with interactive components and specialized features
- Content Reusability: Create reusable content blocks and templates that maintain consistency across large documentation systems
- Domain-Specific Language: Develop custom syntax that matches your organization’s specific content management requirements
- Integration Flexibility: Seamlessly integrate with existing content management systems and development workflows
- Performance Optimization: Implement efficient content processing and rendering strategies for complex interactive features
Foundation Custom Directive Concepts
Understanding Directive Syntax Patterns
Common patterns for implementing custom Markdown directives:
<!-- Block-level directives -->
:::info
This is an information callout block with custom styling and behavior.
:::
<!-- Inline directives -->
This text contains a :badge[Important]{color=red} inline directive.
<!-- Attribute-based directives -->
{.custom-class #custom-id data-component="interactive"}
This paragraph has custom attributes applied.
<!-- Shortcode-style directives -->
{{< alert type="warning" >}}
This is a shortcode-style warning alert.
{{< /alert >}}
<!-- Fence-style directives -->
type: bar
data: [10, 20, 30, 40]
title: Sample Chart Data
### Core Directive Processing Architecture
```javascript
// directive-processor.js - Foundation for custom directive processing
const unified = require('unified');
const remarkParse = require('remark-parse');
const remarkStringify = require('remark-stringify');
const { visit } = require('unist-util-visit');
class MarkdownDirectiveProcessor {
constructor(options = {}) {
this.options = {
enableCustomSyntax: options.enableCustomSyntax !== false,
allowUnsafeDirectives: options.allowUnsafeDirectives || false,
customDirectives: options.customDirectives || new Map(),
preprocessors: options.preprocessors || [],
postprocessors: options.postprocessors || [],
...options
};
this.processor = this.createProcessor();
this.directiveHandlers = new Map();
this.setupDefaultDirectives();
}
createProcessor() {
return unified()
.use(remarkParse)
.use(this.createDirectivePlugin())
.use(remarkStringify);
}
createDirectivePlugin() {
const self = this;
return function directivePlugin() {
return function transformer(tree, file) {
// Process preprocessors first
for (const preprocessor of self.options.preprocessors) {
tree = preprocessor(tree, file) || tree;
}
// Process custom directives
self.processDirectives(tree, file);
// Process postprocessors
for (const postprocessor of self.options.postprocessors) {
tree = postprocessor(tree, file) || tree;
}
return tree;
};
};
}
processDirectives(tree, file) {
// Process block-level directives (container syntax)
visit(tree, 'containerDirective', (node, index, parent) => {
this.handleDirective(node, 'container', index, parent, file);
});
// Process leaf directives (single-line)
visit(tree, 'leafDirective', (node, index, parent) => {
this.handleDirective(node, 'leaf', index, parent, file);
});
// Process text directives (inline)
visit(tree, 'textDirective', (node, index, parent) => {
this.handleDirective(node, 'text', index, parent, file);
});
// Process custom fence blocks
visit(tree, 'code', (node, index, parent) => {
if (node.lang && this.directiveHandlers.has(`fence:${node.lang}`)) {
this.handleDirective({
type: 'fenceDirective',
name: node.lang,
value: node.value,
attributes: {}
}, 'fence', index, parent, file);
}
});
// Process comment-based directives
visit(tree, 'html', (node, index, parent) => {
const commentMatch = node.value.match(/<!--\s*(\w+):\s*(.+?)\s*-->/);
if (commentMatch) {
this.handleDirective({
type: 'commentDirective',
name: commentMatch[1].toLowerCase(),
value: commentMatch[2],
attributes: {}
}, 'comment', index, parent, file);
}
});
}
handleDirective(node, directiveType, index, parent, file) {
const directiveName = node.name.toLowerCase();
const handlerKey = `${directiveType}:${directiveName}`;
if (this.directiveHandlers.has(handlerKey)) {
try {
const handler = this.directiveHandlers.get(handlerKey);
const result = handler(node, {
type: directiveType,
index,
parent,
file,
processor: this
});
if (result && parent && typeof index === 'number') {
parent.children[index] = result;
}
} catch (error) {
console.error(`Error processing directive ${directiveName}:`, error);
// Create error node
parent.children[index] = {
type: 'paragraph',
children: [{
type: 'text',
value: `[Directive Error: ${directiveName} - ${error.message}]`
}]
};
}
}
}
registerDirective(name, type, handler) {
const key = `${type}:${name.toLowerCase()}`;
this.directiveHandlers.set(key, handler);
}
setupDefaultDirectives() {
// Alert/callout directives
this.registerDirective('info', 'container', this.createAlertHandler('info'));
this.registerDirective('warning', 'container', this.createAlertHandler('warning'));
this.registerDirective('error', 'container', this.createAlertHandler('error'));
this.registerDirective('success', 'container', this.createAlertHandler('success'));
// Inline formatting directives
this.registerDirective('badge', 'text', this.createBadgeHandler());
this.registerDirective('highlight', 'text', this.createHighlightHandler());
// Content inclusion directives
this.registerDirective('include', 'comment', this.createIncludeHandler());
this.registerDirective('template', 'comment', this.createTemplateHandler());
// Interactive component directives
this.registerDirective('button', 'leaf', this.createButtonHandler());
this.registerDirective('tabs', 'container', this.createTabsHandler());
// Data visualization directives
this.registerDirective('chart', 'fence', this.createChartHandler());
this.registerDirective('diagram', 'fence', this.createDiagramHandler());
}
createAlertHandler(alertType) {
return (node, context) => {
const title = node.attributes?.title || null;
const className = `alert alert-${alertType}`;
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: [className],
'data-alert-type': alertType
}
},
children: [
...(title ? [{
type: 'strong',
children: [{ type: 'text', value: title }]
}, { type: 'text', value: '\n' }] : []),
...node.children
]
};
};
}
createBadgeHandler() {
return (node, context) => {
const color = node.attributes?.color || 'primary';
const size = node.attributes?.size || 'normal';
return {
type: 'text',
data: {
hName: 'span',
hProperties: {
className: [`badge badge-${color} badge-${size}`],
'data-badge': true
}
},
value: node.children[0]?.value || ''
};
};
}
createHighlightHandler() {
return (node, context) => {
const color = node.attributes?.color || 'yellow';
return {
type: 'text',
data: {
hName: 'mark',
hProperties: {
className: [`highlight highlight-${color}`],
'data-highlight': true
}
},
value: node.children[0]?.value || ''
};
};
}
createIncludeHandler() {
return (node, context) => {
const filePath = node.value.trim();
try {
const fs = require('fs');
const path = require('path');
// Security check - only allow relative paths
if (path.isAbsolute(filePath) || filePath.includes('..')) {
throw new Error('Absolute paths and parent directory access not allowed');
}
const fullPath = path.resolve(context.file.dirname || process.cwd(), filePath);
const includedContent = fs.readFileSync(fullPath, 'utf8');
// Parse included content
const includedTree = this.processor.parse(includedContent);
return {
type: 'root',
children: includedTree.children
};
} catch (error) {
return {
type: 'paragraph',
children: [{
type: 'text',
value: `[Include Error: ${filePath} - ${error.message}]`
}]
};
}
};
}
createTemplateHandler() {
return (node, context) => {
const templateName = node.value.trim();
// Template processing would integrate with your template engine
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['template-placeholder'],
'data-template': templateName
}
},
children: [{
type: 'text',
value: `[Template: ${templateName}]`
}]
};
};
}
createButtonHandler() {
return (node, context) => {
const text = node.children[0]?.value || 'Button';
const variant = node.attributes?.variant || 'primary';
const size = node.attributes?.size || 'medium';
const href = node.attributes?.href;
const onclick = node.attributes?.onclick;
const element = href ? 'a' : 'button';
const properties = {
className: [`btn btn-${variant} btn-${size}`],
'data-button': true
};
if (href) {
properties.href = href;
}
if (onclick && this.options.allowUnsafeDirectives) {
properties.onclick = onclick;
}
return {
type: 'text',
data: {
hName: element,
hProperties: properties
},
value: text
};
};
}
createTabsHandler() {
return (node, context) => {
const tabs = [];
const tabContent = [];
// Process tab children
visit(node, 'containerDirective', (tabNode) => {
if (tabNode.name === 'tab') {
const title = tabNode.attributes?.title || `Tab ${tabs.length + 1}`;
tabs.push(title);
tabContent.push(tabNode.children);
}
});
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['tabs-container'],
'data-tabs': JSON.stringify(tabs)
}
},
children: [{
type: 'text',
value: `[Interactive Tabs Component: ${tabs.join(', ')}]`
}]
};
};
}
createChartHandler() {
return (node, context) => {
try {
const config = this.parseChartConfig(node.value);
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['chart-container'],
'data-chart-config': JSON.stringify(config)
}
},
children: [{
type: 'text',
value: `[Chart: ${config.type || 'unknown'}]`
}]
};
} catch (error) {
return {
type: 'paragraph',
children: [{
type: 'text',
value: `[Chart Error: ${error.message}]`
}]
};
}
};
}
createDiagramHandler() {
return (node, context) => {
const diagramType = node.attributes?.type || 'flowchart';
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['diagram-container'],
'data-diagram-type': diagramType,
'data-diagram-content': node.value
}
},
children: [{
type: 'text',
value: `[Diagram: ${diagramType}]`
}]
};
};
}
parseChartConfig(configString) {
const config = {};
const lines = configString.trim().split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length > 0) {
const value = valueParts.join(':').trim();
// Try to parse as JSON for arrays/objects
try {
config[key.trim()] = JSON.parse(value);
} catch {
config[key.trim()] = value;
}
}
}
return config;
}
async process(markdown, file = {}) {
try {
const result = await this.processor.process({
value: markdown,
path: file.path,
dirname: file.dirname
});
return result.toString();
} catch (error) {
console.error('Markdown processing error:', error);
throw error;
}
}
addPreprocessor(preprocessor) {
this.options.preprocessors.push(preprocessor);
}
addPostprocessor(postprocessor) {
this.options.postprocessors.push(postprocessor);
}
getRegisteredDirectives() {
return Array.from(this.directiveHandlers.keys()).map(key => {
const [type, name] = key.split(':');
return { type, name };
});
}
}
module.exports = MarkdownDirectiveProcessor;
Advanced Directive Implementation Patterns
Interactive Component Directives
// interactive-directives.js - Advanced interactive component system
class InteractiveDirectiveSystem {
constructor(processor) {
this.processor = processor;
this.componentRegistry = new Map();
this.setupInteractiveDirectives();
}
setupInteractiveDirectives() {
// Collapsible content directive
this.processor.registerDirective('details', 'container', (node, context) => {
const summary = node.attributes?.summary || 'Details';
const open = node.attributes?.open === 'true';
return {
type: 'paragraph',
data: {
hName: 'details',
hProperties: {
className: ['collapsible-content'],
open: open || undefined
}
},
children: [
{
type: 'text',
data: {
hName: 'summary',
hProperties: { className: ['collapsible-summary'] }
},
value: summary
},
...node.children
]
};
});
// Code playground directive
this.processor.registerDirective('playground', 'fence', (node, context) => {
const language = node.name;
const code = node.value;
const editable = node.attributes?.editable !== 'false';
const runnable = node.attributes?.runnable === 'true';
const playgroundConfig = {
language,
code,
editable,
runnable,
theme: node.attributes?.theme || 'light'
};
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['code-playground'],
'data-playground-config': JSON.stringify(playgroundConfig)
}
},
children: [{
type: 'text',
value: `[Code Playground: ${language}]`
}]
};
});
// Form input directive
this.processor.registerDirective('input', 'leaf', (node, context) => {
const type = node.attributes?.type || 'text';
const name = node.attributes?.name || 'input';
const placeholder = node.attributes?.placeholder || '';
const required = node.attributes?.required === 'true';
return {
type: 'text',
data: {
hName: 'input',
hProperties: {
type,
name,
placeholder,
required: required || undefined,
className: ['form-input']
}
},
value: ''
};
});
// Progress bar directive
this.processor.registerDirective('progress', 'leaf', (node, context) => {
const value = parseInt(node.attributes?.value || '0');
const max = parseInt(node.attributes?.max || '100');
const label = node.attributes?.label || `${value}%`;
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['progress-container']
}
},
children: [
{
type: 'text',
data: {
hName: 'progress',
hProperties: {
value,
max,
className: ['progress-bar']
}
},
value: ''
},
{
type: 'text',
data: {
hName: 'span',
hProperties: {
className: ['progress-label']
}
},
value: label
}
]
};
});
// Video embed directive
this.processor.registerDirective('video', 'leaf', (node, context) => {
const src = node.attributes?.src;
const poster = node.attributes?.poster;
const controls = node.attributes?.controls !== 'false';
const autoplay = node.attributes?.autoplay === 'true';
const muted = node.attributes?.muted === 'true';
if (!src) {
return {
type: 'paragraph',
children: [{
type: 'text',
value: '[Video Error: No source specified]'
}]
};
}
const properties = {
src,
controls: controls || undefined,
autoplay: autoplay || undefined,
muted: muted || undefined,
className: ['embedded-video']
};
if (poster) {
properties.poster = poster;
}
return {
type: 'text',
data: {
hName: 'video',
hProperties: properties
},
value: ''
};
});
// Card layout directive
this.processor.registerDirective('card', 'container', (node, context) => {
const title = node.attributes?.title;
const image = node.attributes?.image;
const variant = node.attributes?.variant || 'default';
const cardChildren = [];
if (image) {
cardChildren.push({
type: 'text',
data: {
hName: 'img',
hProperties: {
src: image,
className: ['card-image'],
alt: title || 'Card image'
}
},
value: ''
});
}
if (title) {
cardChildren.push({
type: 'text',
data: {
hName: 'h3',
hProperties: {
className: ['card-title']
}
},
value: title
});
}
cardChildren.push({
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['card-content']
}
},
children: node.children
});
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: [`card card-${variant}`]
}
},
children: cardChildren
};
});
}
// Register custom component renderer
registerComponent(name, renderer) {
this.componentRegistry.set(name, renderer);
this.processor.registerDirective(name, 'container', (node, context) => {
try {
return renderer(node, context);
} catch (error) {
return {
type: 'paragraph',
children: [{
type: 'text',
value: `[Component Error: ${name} - ${error.message}]`
}]
};
}
});
}
// Validate component configuration
validateComponent(name, config) {
const component = this.componentRegistry.get(name);
if (!component) {
throw new Error(`Component ${name} not registered`);
}
if (component.validate) {
return component.validate(config);
}
return true;
}
}
// Example usage with custom components
const processor = new MarkdownDirectiveProcessor();
const interactiveSystem = new InteractiveDirectiveSystem(processor);
// Register custom gallery component
interactiveSystem.registerComponent('gallery', (node, context) => {
const images = [];
// Extract image information from children
visit(node, 'image', (imageNode) => {
images.push({
src: imageNode.url,
alt: imageNode.alt || '',
title: imageNode.title || ''
});
});
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['image-gallery'],
'data-gallery-config': JSON.stringify({
images,
layout: node.attributes?.layout || 'grid',
columns: parseInt(node.attributes?.columns || '3')
})
}
},
children: [{
type: 'text',
value: `[Image Gallery: ${images.length} images]`
}]
};
});
module.exports = InteractiveDirectiveSystem;
Conditional Content and Dynamic Processing
// conditional-directives.js - Conditional content and dynamic processing
class ConditionalDirectiveSystem {
constructor(processor, context = {}) {
this.processor = processor;
this.context = context; // User context, environment variables, etc.
this.setupConditionalDirectives();
}
setupConditionalDirectives() {
// Conditional content directive
this.processor.registerDirective('if', 'container', (node, context) => {
const condition = node.attributes?.condition;
const negate = node.attributes?.negate === 'true';
if (!condition) {
return this.createErrorNode('if directive requires a condition attribute');
}
try {
const result = this.evaluateCondition(condition);
const showContent = negate ? !result : result;
if (showContent) {
return {
type: 'root',
children: node.children
};
} else {
return null; // Content is hidden
}
} catch (error) {
return this.createErrorNode(`Condition evaluation error: ${error.message}`);
}
});
// Environment-based content directive
this.processor.registerDirective('env', 'container', (node, context) => {
const env = node.attributes?.env;
const currentEnv = this.context.environment || 'production';
if (!env) {
return this.createErrorNode('env directive requires an env attribute');
}
const envList = env.split(',').map(e => e.trim());
if (envList.includes(currentEnv)) {
return {
type: 'root',
children: node.children
};
}
return null; // Content hidden for this environment
});
// User role-based content directive
this.processor.registerDirective('role', 'container', (node, context) => {
const requiredRoles = node.attributes?.roles?.split(',').map(r => r.trim()) || [];
const userRoles = this.context.userRoles || [];
const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role));
if (hasRequiredRole) {
return {
type: 'root',
children: node.children
};
}
return null; // Content hidden for this user
});
// Feature flag directive
this.processor.registerDirective('feature', 'container', (node, context) => {
const featureName = node.attributes?.name;
const enabledFeatures = this.context.features || {};
if (!featureName) {
return this.createErrorNode('feature directive requires a name attribute');
}
if (enabledFeatures[featureName]) {
return {
type: 'root',
children: node.children
};
}
return null; // Feature not enabled
});
// Dynamic content replacement directive
this.processor.registerDirective('replace', 'text', (node, context) => {
const key = node.attributes?.key;
const defaultValue = node.attributes?.default || '';
if (!key) {
return this.createErrorNode('replace directive requires a key attribute');
}
const value = this.getContextValue(key) || defaultValue;
return {
type: 'text',
value: String(value)
};
});
// Loop directive for repeating content
this.processor.registerDirective('each', 'container', (node, context) => {
const items = node.attributes?.items;
const itemVar = node.attributes?.as || 'item';
if (!items) {
return this.createErrorNode('each directive requires an items attribute');
}
try {
const itemArray = this.getContextValue(items) || [];
if (!Array.isArray(itemArray)) {
throw new Error('Items must be an array');
}
const repeatedContent = [];
for (let i = 0; i < itemArray.length; i++) {
const item = itemArray[i];
const itemContext = {
...this.context,
[itemVar]: item,
[`${itemVar}Index`]: i,
[`${itemVar}First`]: i === 0,
[`${itemVar}Last`]: i === itemArray.length - 1
};
// Process template with item context
const processedChildren = this.processTemplate(node.children, itemContext);
repeatedContent.push(...processedChildren);
}
return {
type: 'root',
children: repeatedContent
};
} catch (error) {
return this.createErrorNode(`Loop processing error: ${error.message}`);
}
});
// Switch/case directive
this.processor.registerDirective('switch', 'container', (node, context) => {
const value = node.attributes?.value;
if (!value) {
return this.createErrorNode('switch directive requires a value attribute');
}
const switchValue = this.getContextValue(value);
let matchedCase = null;
let defaultCase = null;
// Find matching case or default
visit(node, 'containerDirective', (caseNode) => {
if (caseNode.name === 'case') {
const caseValue = caseNode.attributes?.value;
if (caseValue === String(switchValue)) {
matchedCase = caseNode;
}
} else if (caseNode.name === 'default') {
defaultCase = caseNode;
}
});
const selectedCase = matchedCase || defaultCase;
if (selectedCase) {
return {
type: 'root',
children: selectedCase.children
};
}
return null; // No matching case
});
}
evaluateCondition(condition) {
// Simple condition evaluation - extend for more complex logic
const operators = {
'==': (a, b) => a == b,
'===': (a, b) => a === b,
'!=': (a, b) => a != b,
'!==': (a, b) => a !== b,
'>': (a, b) => a > b,
'>=': (a, b) => a >= b,
'<': (a, b) => a < b,
'<=': (a, b) => a <= b,
'includes': (a, b) => String(a).includes(String(b)),
'startswith': (a, b) => String(a).startsWith(String(b)),
'endswith': (a, b) => String(a).endsWith(String(b))
};
// Parse condition: "context.key operator value"
const conditionRegex = /^(\w+(?:\.\w+)*)\s*(==|===|!=|!==|>=|<=|>|<|includes|startswith|endswith)\s*(.+)$/;
const match = condition.match(conditionRegex);
if (!match) {
// Simple boolean check
return Boolean(this.getContextValue(condition));
}
const [, keyPath, operator, value] = match;
const contextValue = this.getContextValue(keyPath);
const comparisonValue = this.parseValue(value);
const operatorFn = operators[operator];
if (!operatorFn) {
throw new Error(`Unknown operator: ${operator}`);
}
return operatorFn(contextValue, comparisonValue);
}
getContextValue(keyPath) {
const keys = keyPath.split('.');
let value = this.context;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return undefined;
}
}
return value;
}
parseValue(value) {
// Try to parse as JSON first
try {
return JSON.parse(value);
} catch {
// Return as string if not valid JSON
return value.replace(/^["']|["']$/g, ''); // Remove quotes
}
}
processTemplate(children, itemContext) {
// Process template variables in text nodes
const processed = [];
for (const child of children) {
if (child.type === 'text') {
processed.push({
...child,
value: this.replaceVariables(child.value, itemContext)
});
} else {
processed.push({
...child,
children: child.children ? this.processTemplate(child.children, itemContext) : []
});
}
}
return processed;
}
replaceVariables(text, context) {
return text.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, keyPath) => {
const value = this.getContextValueFromObject(keyPath, context);
return value !== undefined ? String(value) : match;
});
}
getContextValueFromObject(keyPath, context) {
const keys = keyPath.split('.');
let value = context;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return undefined;
}
}
return value;
}
createErrorNode(message) {
return {
type: 'paragraph',
children: [{
type: 'text',
value: `[Directive Error: ${message}]`
}]
};
}
updateContext(newContext) {
this.context = { ...this.context, ...newContext };
}
}
module.exports = ConditionalDirectiveSystem;
Security and Performance Considerations
Secure Directive Processing
// secure-directives.js - Security-focused directive processing
const DOMPurify = require('isomorphic-dompurify');
const { JSDOM } = require('jsdom');
class SecureDirectiveProcessor {
constructor(processor, options = {}) {
this.processor = processor;
this.options = {
allowedDirectives: options.allowedDirectives || [],
blockedDirectives: options.blockedDirectives || [],
maxProcessingTime: options.maxProcessingTime || 5000, // 5 seconds
maxContentLength: options.maxContentLength || 1024 * 1024, // 1MB
sanitizeOutput: options.sanitizeOutput !== false,
allowUnsafeEval: options.allowUnsafeEval || false,
...options
};
this.setupSecurityMeasures();
}
setupSecurityMeasures() {
// Override processor methods with security checks
const originalRegisterDirective = this.processor.registerDirective.bind(this.processor);
this.processor.registerDirective = (name, type, handler) => {
if (!this.isDirectiveAllowed(name)) {
console.warn(`Directive ${name} is not allowed`);
return;
}
if (this.isDirectiveBlocked(name)) {
console.warn(`Directive ${name} is explicitly blocked`);
return;
}
const secureHandler = this.wrapHandlerWithSecurity(handler, name);
originalRegisterDirective(name, type, secureHandler);
};
// Override process method with additional security
const originalProcess = this.processor.process.bind(this.processor);
this.processor.process = async (markdown, file) => {
// Content length check
if (markdown.length > this.options.maxContentLength) {
throw new Error(`Content too large: ${markdown.length} bytes exceeds limit of ${this.options.maxContentLength} bytes`);
}
// Processing timeout
const processPromise = originalProcess(markdown, file);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Processing timeout')), this.options.maxProcessingTime);
});
const result = await Promise.race([processPromise, timeoutPromise]);
if (this.options.sanitizeOutput) {
return this.sanitizeHtml(result);
}
return result;
};
}
isDirectiveAllowed(name) {
if (this.options.allowedDirectives.length === 0) {
return true; // Allow all if no restrictions
}
return this.options.allowedDirectives.includes(name);
}
isDirectiveBlocked(name) {
return this.options.blockedDirectives.includes(name);
}
wrapHandlerWithSecurity(handler, directiveName) {
return (node, context) => {
try {
// Validate node structure
this.validateNode(node, directiveName);
// Execute handler with timeout
const startTime = Date.now();
const result = handler(node, context);
const endTime = Date.now();
if (endTime - startTime > 1000) { // 1 second warning
console.warn(`Directive ${directiveName} took ${endTime - startTime}ms to process`);
}
// Validate result
if (result) {
this.validateDirectiveOutput(result, directiveName);
}
return result;
} catch (error) {
console.error(`Security error in directive ${directiveName}:`, error);
return {
type: 'paragraph',
children: [{
type: 'text',
value: `[Security Error: ${directiveName}]`
}]
};
}
};
}
validateNode(node, directiveName) {
// Check for suspicious attributes
if (node.attributes) {
for (const [key, value] of Object.entries(node.attributes)) {
// Block script-related attributes
if (['onclick', 'onload', 'onerror', 'onmouseover'].includes(key.toLowerCase())) {
if (!this.options.allowUnsafeEval) {
throw new Error(`Unsafe attribute ${key} not allowed in directive ${directiveName}`);
}
}
// Check for suspicious values
if (typeof value === 'string' && this.containsSuspiciousContent(value)) {
console.warn(`Potentially unsafe content in attribute ${key} of directive ${directiveName}`);
}
}
}
// Validate children content
if (node.children) {
this.validateChildren(node.children, directiveName);
}
}
validateChildren(children, directiveName) {
const visit = require('unist-util-visit');
visit({ type: 'root', children }, (child) => {
if (child.type === 'html') {
if (!this.isHtmlSafe(child.value)) {
throw new Error(`Unsafe HTML detected in directive ${directiveName}`);
}
}
if (child.value && this.containsSuspiciousContent(child.value)) {
console.warn(`Potentially suspicious content in directive ${directiveName}`);
}
});
}
containsSuspiciousContent(content) {
const suspiciousPatterns = [
/<script[^>]*>/i,
/javascript:/i,
/data:text\/html/i,
/vbscript:/i,
/on\w+\s*=/i,
/eval\s*\(/i,
/Function\s*\(/i
];
return suspiciousPatterns.some(pattern => pattern.test(content));
}
isHtmlSafe(html) {
// Basic HTML safety check
const dangerousTags = ['script', 'iframe', 'object', 'embed', 'form'];
const tagPattern = /<(\w+)[^>]*>/gi;
let match;
while ((match = tagPattern.exec(html)) !== null) {
if (dangerousTags.includes(match[1].toLowerCase())) {
return false;
}
}
return true;
}
validateDirectiveOutput(output, directiveName) {
if (!output || typeof output !== 'object') {
return;
}
// Check for suspicious data properties
if (output.data && output.data.hProperties) {
const props = output.data.hProperties;
for (const [key, value] of Object.entries(props)) {
if (key.startsWith('on') && !this.options.allowUnsafeEval) {
delete props[key];
console.warn(`Removed unsafe property ${key} from directive ${directiveName} output`);
}
}
}
// Recursively validate children
if (output.children) {
this.validateChildren(output.children, directiveName);
}
}
sanitizeHtml(html) {
if (typeof html !== 'string') {
return html;
}
try {
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const config = {
ALLOWED_TAGS: [
'p', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'img', 'code', 'pre', 'blockquote',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'br', 'hr',
'strong', 'em', 'u', 's', 'mark', 'details', 'summary'
],
ALLOWED_ATTR: [
'href', 'src', 'alt', 'title', 'class', 'id', 'data-*',
'target', 'rel', 'width', 'height'
],
ALLOW_DATA_ATTR: true
};
return purify.sanitize(html, config);
} catch (error) {
console.error('HTML sanitization error:', error);
return html; // Return original on error
}
}
// Rate limiting for directive processing
createRateLimiter(maxRequests = 100, windowMs = 60000) {
const requests = new Map();
return (directiveName) => {
const now = Date.now();
const key = directiveName;
if (!requests.has(key)) {
requests.set(key, []);
}
const directiveRequests = requests.get(key);
// Clean old requests
const validRequests = directiveRequests.filter(time => now - time < windowMs);
if (validRequests.length >= maxRequests) {
throw new Error(`Rate limit exceeded for directive ${directiveName}`);
}
validRequests.push(now);
requests.set(key, validRequests);
};
}
}
module.exports = SecureDirectiveProcessor;
Integration with Modern Development Workflows
Markdown custom directives integrate seamlessly with comprehensive content management systems. When combined with automated content validation and quality assurance systems, custom directives enable sophisticated content processing pipelines that maintain quality and consistency while providing advanced interactive features for complex documentation and content platforms.
For advanced content presentation, custom directives work effectively with responsive design and mobile optimization techniques to create adaptive content experiences that render consistently across devices while leveraging device-specific capabilities through conditional directive processing.
When building comprehensive documentation platforms, custom directives complement search optimization and indexing systems to create searchable, interactive content that maintains SEO performance while providing rich user interactions and dynamic content generation capabilities.
Real-World Implementation Examples
Documentation Platform Integration
// docs-platform-integration.js - Complete documentation platform with custom directives
class DocumentationPlatform {
constructor(options = {}) {
this.options = {
baseUrl: options.baseUrl || '',
apiEndpoint: options.apiEndpoint || '/api',
enableInteractive: options.enableInteractive !== false,
userContext: options.userContext || {},
...options
};
this.processor = new MarkdownDirectiveProcessor({
allowUnsafeDirectives: false,
customDirectives: new Map()
});
this.setupDocumentationDirectives();
}
setupDocumentationDirectives() {
// API documentation directive
this.processor.registerDirective('api', 'container', (node, context) => {
const method = node.attributes?.method?.toUpperCase() || 'GET';
const endpoint = node.attributes?.endpoint || '';
const title = node.attributes?.title || `${method} ${endpoint}`;
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['api-documentation'],
'data-api-method': method,
'data-api-endpoint': endpoint
}
},
children: [
{
type: 'text',
data: {
hName: 'h4',
hProperties: { className: ['api-title'] }
},
value: title
},
{
type: 'paragraph',
data: {
hName: 'div',
hProperties: { className: ['api-method-badge', `method-${method.toLowerCase()}`] }
},
children: [{ type: 'text', value: method }]
},
{
type: 'paragraph',
data: {
hName: 'code',
hProperties: { className: ['api-endpoint'] }
},
children: [{ type: 'text', value: endpoint }]
},
...node.children
]
};
});
// Interactive code example directive
this.processor.registerDirective('example', 'fence', (node, context) => {
const language = node.name || 'javascript';
const runnable = node.attributes?.runnable === 'true';
const editable = node.attributes?.editable === 'true';
const showOutput = node.attributes?.output === 'true';
const exampleConfig = {
language,
code: node.value,
runnable,
editable,
showOutput,
id: `example-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
};
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['interactive-example'],
'data-example-config': JSON.stringify(exampleConfig)
}
},
children: [{
type: 'text',
value: `[Interactive Example: ${language}]`
}]
};
});
// Tabbed content directive
this.processor.registerDirective('tabs', 'container', (node, context) => {
const tabs = [];
let currentTab = null;
for (const child of node.children) {
if (child.type === 'containerDirective' && child.name === 'tab') {
if (currentTab) {
tabs.push(currentTab);
}
currentTab = {
title: child.attributes?.title || `Tab ${tabs.length + 1}`,
id: child.attributes?.id || `tab-${tabs.length}`,
content: child.children
};
} else if (currentTab) {
currentTab.content.push(child);
}
}
if (currentTab) {
tabs.push(currentTab);
}
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['tabbed-content'],
'data-tabs': JSON.stringify(tabs.map(tab => ({ title: tab.title, id: tab.id })))
}
},
children: tabs.map(tab => ({
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['tab-panel'],
'data-tab-id': tab.id
}
},
children: tab.content
}))
};
});
// Version-specific content directive
this.processor.registerDirective('version', 'container', (node, context) => {
const minVersion = node.attributes?.min;
const maxVersion = node.attributes?.max;
const currentVersion = this.options.userContext.version || '1.0.0';
let showContent = true;
if (minVersion && this.compareVersions(currentVersion, minVersion) < 0) {
showContent = false;
}
if (maxVersion && this.compareVersions(currentVersion, maxVersion) > 0) {
showContent = false;
}
if (showContent) {
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['version-specific'],
'data-min-version': minVersion,
'data-max-version': maxVersion
}
},
children: node.children
};
}
return null; // Hide content for this version
});
// Cross-reference directive
this.processor.registerDirective('xref', 'text', (node, context) => {
const target = node.attributes?.target;
const type = node.attributes?.type || 'page';
const text = node.children[0]?.value || target;
if (!target) {
return {
type: 'text',
value: '[Cross-reference error: no target]'
};
}
const href = this.resolveReference(target, type);
return {
type: 'text',
data: {
hName: 'a',
hProperties: {
href,
className: ['cross-reference', `xref-${type}`],
'data-xref-target': target
}
},
value: text
};
});
// Glossary term directive
this.processor.registerDirective('term', 'text', (node, context) => {
const term = node.children[0]?.value || '';
const definition = node.attributes?.definition;
return {
type: 'text',
data: {
hName: 'abbr',
hProperties: {
className: ['glossary-term'],
title: definition,
'data-term': term
}
},
value: term
};
});
// Changelog entry directive
this.processor.registerDirective('changelog', 'container', (node, context) => {
const version = node.attributes?.version;
const date = node.attributes?.date;
const type = node.attributes?.type || 'general'; // general, breaking, feature, fix
return {
type: 'paragraph',
data: {
hName: 'div',
hProperties: {
className: ['changelog-entry', `changelog-${type}`],
'data-version': version,
'data-date': date
}
},
children: [
{
type: 'text',
data: {
hName: 'h5',
hProperties: { className: ['changelog-header'] }
},
value: `${version} - ${date}`
},
{
type: 'paragraph',
data: {
hName: 'div',
hProperties: { className: ['changelog-content'] }
},
children: node.children
}
]
};
});
}
compareVersions(version1, version2) {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part < v2Part) return -1;
if (v1Part > v2Part) return 1;
}
return 0;
}
resolveReference(target, type) {
switch (type) {
case 'page':
return `${this.options.baseUrl}/${target}`;
case 'api':
return `${this.options.baseUrl}/api/${target}`;
case 'guide':
return `${this.options.baseUrl}/guides/${target}`;
case 'example':
return `${this.options.baseUrl}/examples/${target}`;
default:
return `${this.options.baseUrl}/${target}`;
}
}
async processDocument(markdown, context = {}) {
const fullContext = {
...this.options.userContext,
...context
};
// Create conditional processor with user context
const conditionalProcessor = new ConditionalDirectiveSystem(this.processor, fullContext);
try {
const result = await this.processor.process(markdown);
return result;
} catch (error) {
console.error('Document processing error:', error);
throw error;
}
}
generateDirectiveDocumentation() {
const directives = this.processor.getRegisteredDirectives();
const docs = {
overview: 'Custom directives available in this documentation platform',
directives: []
};
for (const directive of directives) {
docs.directives.push({
name: directive.name,
type: directive.type,
description: this.getDirectiveDescription(directive.name),
syntax: this.getDirectiveSyntax(directive.name, directive.type)
});
}
return docs;
}
getDirectiveDescription(name) {
const descriptions = {
api: 'Documents API endpoints with method, path, and description',
example: 'Creates interactive code examples with optional execution',
tabs: 'Creates tabbed content sections',
version: 'Shows content conditionally based on version requirements',
xref: 'Creates cross-references to other documentation sections',
term: 'Defines glossary terms with hover definitions',
changelog: 'Formats changelog entries with version and date information'
};
return descriptions[name] || 'Custom directive';
}
getDirectiveSyntax(name, type) {
const syntaxExamples = {
api: ':::api{method="POST" endpoint="/users" title="Create User"}\nAPI description here\n:::',
example: '```example{runnable="true" editable="true"}\nconsole.log("Hello World");\n```',
tabs: ':::tabs\n:::tab{title="JavaScript"}\nJS content\n:::\n:::tab{title="Python"}\nPython content\n:::\n:::',
version: ':::version{min="2.0.0" max="3.0.0"}\nContent for versions 2.x\n:::',
xref: ':xref[API Reference]{target="api-guide" type="guide"}',
term: ':term[API]{definition="Application Programming Interface"}',
changelog: ':::changelog{version="1.2.0" date="2025-10-31" type="feature"}\nAdded new features\n:::'
};
return syntaxExamples[name] || `${type} directive syntax`;
}
}
module.exports = DocumentationPlatform;
Performance Optimization for Custom Directives
Caching and Memoization Strategies
// directive-performance.js - Performance optimization for directive processing
class PerformantDirectiveProcessor {
constructor(baseProcessor, options = {}) {
this.baseProcessor = baseProcessor;
this.options = {
enableCaching: options.enableCaching !== false,
cacheSize: options.cacheSize || 1000,
enableMemoization: options.enableMemoization !== false,
performanceMonitoring: options.performanceMonitoring || false,
...options
};
// LRU cache for processed results
this.processedCache = new Map();
this.cacheHits = 0;
this.cacheMisses = 0;
// Memoization for directive handlers
this.memoizedHandlers = new Map();
// Performance metrics
this.metrics = {
totalProcessingTime: 0,
directiveProcessingTimes: new Map(),
cacheHitRate: 0,
processedDocuments: 0
};
this.setupPerformanceOptimizations();
}
setupPerformanceOptimizations() {
// Wrap processor methods with caching
const originalProcess = this.baseProcessor.process.bind(this.baseProcessor);
this.baseProcessor.process = async (markdown, context = {}) => {
const startTime = Date.now();
// Generate cache key
const cacheKey = this.generateCacheKey(markdown, context);
// Check cache first
if (this.options.enableCaching && this.processedCache.has(cacheKey)) {
this.cacheHits++;
this.updateCacheHitRate();
return this.processedCache.get(cacheKey);
}
this.cacheMisses++;
// Process document
const result = await originalProcess(markdown, context);
// Cache result
if (this.options.enableCaching) {
this.cacheResult(cacheKey, result);
}
// Update metrics
const endTime = Date.now();
this.updateMetrics(endTime - startTime);
return result;
};
// Wrap directive registration with memoization
const originalRegisterDirective = this.baseProcessor.registerDirective.bind(this.baseProcessor);
this.baseProcessor.registerDirective = (name, type, handler) => {
let optimizedHandler = handler;
if (this.options.enableMemoization) {
optimizedHandler = this.memoizeHandler(handler, `${type}:${name}`);
}
if (this.options.performanceMonitoring) {
optimizedHandler = this.wrapHandlerWithMetrics(optimizedHandler, `${type}:${name}`);
}
originalRegisterDirective(name, type, optimizedHandler);
};
}
generateCacheKey(markdown, context) {
const crypto = require('crypto');
const contentHash = crypto.createHash('md5').update(markdown).digest('hex');
const contextHash = crypto.createHash('md5').update(JSON.stringify(context)).digest('hex');
return `${contentHash}-${contextHash}`;
}
cacheResult(key, result) {
// Implement LRU eviction
if (this.processedCache.size >= this.options.cacheSize) {
const firstKey = this.processedCache.keys().next().value;
this.processedCache.delete(firstKey);
}
this.processedCache.set(key, result);
}
memoizeHandler(handler, handlerKey) {
const memoCache = new Map();
return (node, context) => {
const nodeKey = this.generateNodeKey(node);
const fullKey = `${handlerKey}-${nodeKey}`;
if (memoCache.has(fullKey)) {
return memoCache.get(fullKey);
}
const result = handler(node, context);
// Only cache serializable results
if (this.isSerializable(result)) {
memoCache.set(fullKey, result);
// Limit cache size per handler
if (memoCache.size > 100) {
const firstKey = memoCache.keys().next().value;
memoCache.delete(firstKey);
}
}
return result;
};
}
wrapHandlerWithMetrics(handler, handlerKey) {
return (node, context) => {
const startTime = Date.now();
try {
const result = handler(node, context);
const endTime = Date.now();
this.updateDirectiveMetrics(handlerKey, endTime - startTime, true);
return result;
} catch (error) {
const endTime = Date.now();
this.updateDirectiveMetrics(handlerKey, endTime - startTime, false);
throw error;
}
};
}
generateNodeKey(node) {
const crypto = require('crypto');
const nodeData = {
type: node.type,
name: node.name,
attributes: node.attributes,
childrenCount: node.children ? node.children.length : 0
};
return crypto.createHash('md5').update(JSON.stringify(nodeData)).digest('hex');
}
isSerializable(obj) {
try {
JSON.stringify(obj);
return true;
} catch {
return false;
}
}
updateCacheHitRate() {
const total = this.cacheHits + this.cacheMisses;
this.metrics.cacheHitRate = total > 0 ? this.cacheHits / total : 0;
}
updateMetrics(processingTime) {
this.metrics.totalProcessingTime += processingTime;
this.metrics.processedDocuments++;
}
updateDirectiveMetrics(handlerKey, processingTime, success) {
if (!this.metrics.directiveProcessingTimes.has(handlerKey)) {
this.metrics.directiveProcessingTimes.set(handlerKey, {
totalTime: 0,
callCount: 0,
successCount: 0,
errorCount: 0
});
}
const directiveMetrics = this.metrics.directiveProcessingTimes.get(handlerKey);
directiveMetrics.totalTime += processingTime;
directiveMetrics.callCount++;
if (success) {
directiveMetrics.successCount++;
} else {
directiveMetrics.errorCount++;
}
}
getPerformanceMetrics() {
const avgProcessingTime = this.metrics.processedDocuments > 0
? this.metrics.totalProcessingTime / this.metrics.processedDocuments
: 0;
const directiveStats = {};
for (const [key, stats] of this.metrics.directiveProcessingTimes) {
directiveStats[key] = {
averageTime: stats.callCount > 0 ? stats.totalTime / stats.callCount : 0,
totalCalls: stats.callCount,
successRate: stats.callCount > 0 ? stats.successCount / stats.callCount : 0
};
}
return {
cacheHitRate: this.metrics.cacheHitRate,
averageProcessingTime: avgProcessingTime,
processedDocuments: this.metrics.processedDocuments,
cacheSize: this.processedCache.size,
directivePerformance: directiveStats
};
}
clearCache() {
this.processedCache.clear();
this.memoizedHandlers.clear();
this.cacheHits = 0;
this.cacheMisses = 0;
}
warmupCache(documents) {
return Promise.all(
documents.map(doc => this.baseProcessor.process(doc.content, doc.context))
);
}
}
module.exports = PerformantDirectiveProcessor;
Troubleshooting Common Custom Directive Issues
Debugging and Error Handling
Problem: Directive not processing or throwing errors during compilation
Solutions:
// Enable detailed debugging for directive processing
const processor = new MarkdownDirectiveProcessor({
debug: true,
errorHandling: 'detailed' // 'silent', 'basic', 'detailed'
});
// Add error boundary for directive processing
processor.addPreprocessor((tree, file) => {
console.log('Processing tree:', JSON.stringify(tree, null, 2));
return tree;
});
// Validate directive syntax before processing
const validateDirectiveSyntax = (content) => {
const directivePatterns = {
container: /:::(\w+)(?:\{([^}]*)\})?\s*\n([\s\S]*?)\n:::/g,
leaf: /::(\w+)(?:\{([^}]*)\})?(?:\[([^\]]*)\])?/g,
text: /:(\w+)(?:\{([^}]*)\})?(?:\[([^\]]*)\])?/g
};
const issues = [];
for (const [type, pattern] of Object.entries(directivePatterns)) {
let match;
while ((match = pattern.exec(content)) !== null) {
try {
// Validate attributes syntax
if (match[2]) {
JSON.parse(`{${match[2]}}`);
}
} catch (error) {
issues.push({
type,
directive: match[1],
position: match.index,
error: `Invalid attributes syntax: ${error.message}`
});
}
}
}
return issues;
};
Problem: Performance degradation with complex directive processing
Solutions:
// Implement directive processing timeouts
const createTimoutWrapper = (handler, timeout = 5000) => {
return async (node, context) => {
return Promise.race([
Promise.resolve(handler(node, context)),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Directive processing timeout')), timeout)
)
]);
};
};
// Use streaming for large content processing
const processLargeContent = async (content, chunkSize = 10000) => {
const chunks = [];
for (let i = 0; i < content.length; i += chunkSize) {
chunks.push(content.slice(i, i + chunkSize));
}
const results = [];
for (const chunk of chunks) {
const result = await processor.process(chunk);
results.push(result);
}
return results.join('');
};
Problem: Security vulnerabilities in custom directive implementations
Solutions:
// Implement comprehensive input validation
const validateDirectiveInput = (node, allowedAttributes = []) => {
if (node.attributes) {
for (const [key, value] of Object.entries(node.attributes)) {
// Check allowed attributes
if (allowedAttributes.length > 0 && !allowedAttributes.includes(key)) {
throw new Error(`Attribute '${key}' not allowed`);
}
// Validate attribute values
if (typeof value === 'string') {
// Check for script injection
if (/<script|javascript:|data:text\/html/i.test(value)) {
throw new Error(`Potentially unsafe value in attribute '${key}'`);
}
// Limit attribute value length
if (value.length > 1000) {
throw new Error(`Attribute '${key}' value too long`);
}
}
}
}
return true;
};
// Sanitize directive output
const sanitizeDirectiveOutput = (output) => {
if (output && output.data && output.data.hProperties) {
const props = output.data.hProperties;
// Remove event handlers
Object.keys(props).forEach(key => {
if (key.startsWith('on') || key.toLowerCase().includes('script')) {
delete props[key];
}
});
// Validate href attributes
if (props.href && !isValidUrl(props.href)) {
delete props.href;
}
}
return output;
};
Conclusion
Markdown custom directives and extensions transform static content into dynamic, interactive experiences by extending the fundamental capabilities of Markdown syntax while maintaining compatibility with standard processing workflows. By implementing sophisticated directive processing systems, security measures, and performance optimizations, technical teams can create powerful content management platforms that scale efficiently while providing rich user interactions and domain-specific functionality tailored to organizational needs.
The key to successful custom directive implementation lies in balancing functionality with security, performance with flexibility, and complexity with maintainability. Whether you’re building documentation platforms, content management systems, or specialized publishing workflows, the techniques covered in this guide provide the foundation for creating robust directive systems that enhance content capabilities while preserving the simplicity and portability that makes Markdown such a valuable content creation tool.
Remember to implement comprehensive security validation for user-provided content, optimize performance for large-scale processing, and maintain clear documentation for custom directive syntax and capabilities. With proper implementation of custom directives, your Markdown-based systems can deliver sophisticated content experiences that bridge the gap between simple text formatting and full-featured content management platforms.