A Markdown to HTML converter written in Javascript
—
Event listener system for hooking into the conversion process at various stages, enabling custom processing and monitoring.
Adds an event listener to a Converter instance.
/**
* Listen to an event
* @param name - Event name
* @param callback - Event callback function
* @returns Converter instance for chaining
*/
converter.listen(name: string, callback: EventCallback): showdown.ConverterWhere EventCallback has the signature:
type EventCallback = (
evtName: string,
text: string,
converter: showdown.Converter,
options: ConverterOptions,
globals: any
) => string | voidUsage Examples:
const converter = new showdown.Converter();
// Listen to conversion events
converter.listen('conversion.start', function(evtName, text, converter, options, globals) {
console.log('Starting conversion of', text.length, 'characters');
});
converter.listen('conversion.end', function(evtName, html, converter, options, globals) {
console.log('Generated', html.length, 'characters of HTML');
});
// Listen and modify content
converter.listen('headers.before', function(evtName, text, converter, options, globals) {
// Add custom processing before header parsing
return text.replace(/^# (.+)/gm, '# 📝 $1');
});
// Chaining listeners
converter
.listen('event1', callback1)
.listen('event2', callback2);Events are dispatched at various stages of the conversion process:
// Conversion start/end
converter.listen('conversion.start', function(evtName, text) {
console.log('Conversion started');
});
converter.listen('conversion.end', function(evtName, html) {
console.log('Conversion completed');
});Events fired during different parsing stages:
// Before/after specific parsing stages
converter.listen('headers.before', function(evtName, text) {
console.log('About to process headers');
return text; // Return modified text or undefined to keep original
});
converter.listen('headers.after', function(evtName, text) {
console.log('Headers processed');
});
converter.listen('links.before', function(evtName, text) {
console.log('About to process links');
});
converter.listen('codeBlocks.before', function(evtName, text) {
console.log('About to process code blocks');
});Event callbacks can:
undefined or nothing: Keep original text unchangedconverter.listen('custom.event', function(evtName, text, converter, options, globals) {
// Log event details
console.log('Event:', evtName);
console.log('Text length:', text.length);
console.log('Options:', options);
// Modify text
const modifiedText = text.replace(/\[CUSTOM\]/g, '<span class="custom">CUSTOM</span>');
// Return modified text
return modifiedText;
});While primarily used internally, events can be manually dispatched:
// Access internal dispatch method (not recommended for external use)
const result = converter._dispatch('custom.event', 'some text', options, globals);Events are commonly used in listener-type extensions:
showdown.extension('analytics', {
type: 'listener',
listeners: {
'conversion.start': function(evtName, text, converter) {
analytics.track('markdown.conversion.start', {
textLength: text.length,
options: converter.getOptions()
});
},
'conversion.end': function(evtName, html, converter) {
analytics.track('markdown.conversion.end', {
htmlLength: html.length
});
},
'headers.before': function(evtName, text) {
// Count headers before processing
const headerCount = (text.match(/^#+\s/gm) || []).length;
analytics.track('markdown.headers.count', { count: headerCount });
}
}
});
// Use extension
const converter = new showdown.Converter({
extensions: ['analytics']
});const converter = new showdown.Converter();
// Monitor conversion performance
const stats = { conversions: 0, totalTime: 0 };
converter.listen('conversion.start', function() {
this.startTime = Date.now();
});
converter.listen('conversion.end', function() {
const duration = Date.now() - this.startTime;
stats.conversions++;
stats.totalTime += duration;
console.log(`Conversion ${stats.conversions} took ${duration}ms`);
});const converter = new showdown.Converter();
// Add custom processing
converter.listen('headers.before', function(evtName, text) {
// Add emoji to headers
return text.replace(/^(#+)\s*(.+)/gm, '$1 🎯 $2');
});
converter.listen('links.after', function(evtName, text) {
// Add target="_blank" to external links
return text.replace(
/<a href="(https?:\/\/[^"]+)"/g,
'<a href="$1" target="_blank" rel="noopener"'
);
});const converter = new showdown.Converter();
// Debug event system
const debugEvents = ['conversion.start', 'conversion.end', 'headers.before', 'links.before'];
debugEvents.forEach(eventName => {
converter.listen(eventName, function(evtName, text) {
console.log(`[DEBUG] Event: ${evtName}, Text length: ${text.length}`);
if (text.length < 100) {
console.log(`[DEBUG] Text: ${text}`);
}
});
});const converter = new showdown.Converter();
// Validate content during conversion
converter.listen('conversion.start', function(evtName, text) {
if (text.length > 100000) {
console.warn('Large document detected:', text.length, 'characters');
}
const linkCount = (text.match(/\[([^\]]+)\]\([^)]+\)/g) || []).length;
if (linkCount > 50) {
console.warn('Document contains many links:', linkCount);
}
});converter.listen('custom.event', function(evtName, text) {
try {
// Potentially risky operation
return processText(text);
} catch (error) {
console.error('Event callback error:', error);
// Return original text on error
return text;
}
});Multiple listeners can be registered for the same event:
const converter = new showdown.Converter();
// First listener
converter.listen('conversion.start', function() {
console.log('Listener 1: Conversion starting');
});
// Second listener
converter.listen('conversion.start', function() {
console.log('Listener 2: Conversion starting');
});
// Both will be called in registration orderInstall with Tessl CLI
npx tessl i tessl/npm-showdown