Overview
Inkdown provides APIs to process markdown content both during rendering (post-processors) and for specific code blocks (code block processors).// Post processor - runs on all rendered markdown
this.registerMarkdownPostProcessor((el, ctx) => {
// Process rendered HTML elements
});
// Code block processor - runs on specific language code blocks
this.registerMarkdownCodeBlockProcessor('mermaid', (source, el, ctx) => {
// Process code block content
});
Types
type MarkdownPostProcessor = (
el: IMarkdownElement,
ctx: MarkdownPostProcessorContext
) => void | Promise<void>;
type MarkdownCodeBlockProcessor = (
source: string,
el: IMarkdownElement,
ctx: MarkdownPostProcessorContext
) => void | Promise<void>;
interface MarkdownPostProcessorContext {
sourcePath: string;
frontmatter?: Record<string, unknown>;
getSectionInfo?: (el: IMarkdownElement) => SectionInfo | null;
}
interface SectionInfo {
lineStart: number;
lineEnd: number;
text: string;
}
IMarkdownElement
Abstraction over HTML elements for cross-platform compatibility.interface IMarkdownElement {
addClass(className: string): void;
removeClass(className: string): void;
hasClass(className: string): boolean;
setText(text: string): void;
getText(): string;
setInnerHTML(html: string): void;
getInnerHTML(): string;
setAttribute(name: string, value: string): void;
getAttribute(name: string): string | null;
find(selector: string): IMarkdownElement | null;
findAll(selector: string): IMarkdownElement[];
appendChild(child: IMarkdownElement): void;
remove(): void;
getParent(): IMarkdownElement | null;
getNativeElement(): unknown;
}
Markdown Post Processors
Post processors run on all rendered markdown content.Registration
this.registerMarkdownPostProcessor((el, ctx) => {
// Process the rendered element
console.log('Source:', ctx.sourcePath);
console.log('Frontmatter:', ctx.frontmatter);
});
Example: External Link Icons
this.registerMarkdownPostProcessor((el, ctx) => {
const links = el.findAll('a');
for (const link of links) {
const href = link.getAttribute('href');
if (href && href.startsWith('http')) {
link.addClass('external-link');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
}
}
});
Example: Custom Block Quotes
this.registerMarkdownPostProcessor((el, ctx) => {
const blockquotes = el.findAll('blockquote');
for (const quote of blockquotes) {
const text = quote.getText().trim();
// Add icon for quotes starting with "Note:"
if (text.startsWith('Note:')) {
quote.addClass('note-quote');
const icon = document.createElement('span');
icon.className = 'quote-icon';
icon.textContent = '📝';
quote.getNativeElement().prepend(icon);
}
// Warning quotes
if (text.startsWith('Warning:')) {
quote.addClass('warning-quote');
}
}
});
Example: Highlight Search Terms
this.registerMarkdownPostProcessor((el, ctx) => {
const searchTerm = this.getSearchTerm();
if (!searchTerm) return;
const walker = document.createTreeWalker(
el.getNativeElement(),
NodeFilter.SHOW_TEXT
);
const nodesToReplace: Array<{node: Node; text: string}> = [];
let node;
while (node = walker.nextNode()) {
const text = node.textContent || '';
if (text.toLowerCase().includes(searchTerm.toLowerCase())) {
nodesToReplace.push({node, text});
}
}
for (const {node, text} of nodesToReplace) {
const highlighted = text.replace(
new RegExp(`(${searchTerm})`, 'gi'),
'<mark>$1</mark>'
);
const span = document.createElement('span');
span.innerHTML = highlighted;
node.parentNode?.replaceChild(span, node);
}
});
Example: Process Images
this.registerMarkdownPostProcessor((el, ctx) => {
const images = el.findAll('img');
for (const img of images) {
// Add lazy loading
img.setAttribute('loading', 'lazy');
// Add click to zoom
img.addClass('zoomable');
// Wrap in figure with caption
const alt = img.getAttribute('alt');
if (alt) {
const figure = document.createElement('figure');
const caption = document.createElement('figcaption');
caption.textContent = alt;
const parent = img.getParent();
if (parent) {
parent.appendChild(figure);
figure.appendChild(img.getNativeElement());
figure.appendChild(caption);
}
}
}
});
Code Block Processors
Process code blocks with specific language identifiers.Registration
this.registerMarkdownCodeBlockProcessor(
'mermaid',
(source, el, ctx) => {
// Process mermaid diagram source
el.setText('Diagram: ' + source);
}
);
Example: Mermaid Diagrams
import mermaid from 'mermaid';
this.registerMarkdownCodeBlockProcessor(
'mermaid',
async (source, el, ctx) => {
const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
try {
const {svg} = await mermaid.render(id, source);
el.setInnerHTML(svg);
el.addClass('mermaid-diagram');
} catch (error) {
el.setText('Failed to render diagram');
el.addClass('mermaid-error');
console.error('Mermaid error:', error);
}
}
);
Example: Chart.js Charts
import Chart from 'chart.js';
this.registerMarkdownCodeBlockProcessor(
'chart',
(source, el, ctx) => {
try {
const config = JSON.parse(source);
const canvas = document.createElement('canvas');
el.getNativeElement().appendChild(canvas);
new Chart(canvas, config);
el.addClass('chart-container');
} catch (error) {
el.setText('Invalid chart configuration');
console.error('Chart error:', error);
}
}
);
Example: Math Rendering (KaTeX)
import katex from 'katex';
this.registerMarkdownCodeBlockProcessor(
'math',
(source, el, ctx) => {
try {
const html = katex.renderToString(source, {
displayMode: true,
throwOnError: false,
});
el.setInnerHTML(html);
el.addClass('math-block');
} catch (error) {
el.setText('Math rendering error');
console.error('KaTeX error:', error);
}
}
);
Example: Code Block with Copy Button
this.registerMarkdownCodeBlockProcessor(
'javascript',
(source, el, ctx) => {
const container = document.createElement('div');
container.className = 'code-block-container';
const pre = document.createElement('pre');
const code = document.createElement('code');
code.textContent = source;
pre.appendChild(code);
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
copyButton.className = 'code-copy-btn';
copyButton.onclick = () => {
navigator.clipboard.writeText(source);
copyButton.textContent = 'Copied!';
setTimeout(() => {
copyButton.textContent = 'Copy';
}, 2000);
};
container.appendChild(copyButton);
container.appendChild(pre);
el.getNativeElement().appendChild(container);
}
);
Example: CSV Table
this.registerMarkdownCodeBlockProcessor(
'csv',
(source, el, ctx) => {
const lines = source.trim().split('\n');
const table = document.createElement('table');
table.className = 'csv-table';
// Header
if (lines.length > 0) {
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const headers = lines[0].split(',');
for (const header of headers) {
const th = document.createElement('th');
th.textContent = header.trim();
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
}
// Body
if (lines.length > 1) {
const tbody = document.createElement('tbody');
for (let i = 1; i < lines.length; i++) {
const row = document.createElement('tr');
const cells = lines[i].split(',');
for (const cell of cells) {
const td = document.createElement('td');
td.textContent = cell.trim();
row.appendChild(td);
}
tbody.appendChild(row);
}
table.appendChild(tbody);
}
el.getNativeElement().appendChild(table);
}
);
Emoji Plugin Example
From the built-in Emoji plugin:import { Plugin, type IMarkdownElement } from '@inkdown/api';
import { GITHUB_EMOJI_MAP } from './emojiMap';
const EMOJI_REGEX = /:([a-z0-9_+-]+):/gi;
export default class EmojiPlugin extends Plugin {
async onload() {
// Post processor for preview mode
this.registerMarkdownPostProcessor((el: IMarkdownElement, ctx) => {
el.replaceText(EMOJI_REGEX, (match, emojiCode) => {
const emoji = GITHUB_EMOJI_MAP[emojiCode.toLowerCase()];
return emoji || match;
});
});
// Editor extension for live preview
// (See editor-extensions.mdx)
}
}
Best Practices
Performance
// ✅ Good - efficient selectors
this.registerMarkdownPostProcessor((el, ctx) => {
const links = el.findAll('a[href^="http"]');
// Process only external links
});
// ❌ Bad - process everything
this.registerMarkdownPostProcessor((el, ctx) => {
const allElements = el.findAll('*');
// Too expensive!
});
Error Handling
// ✅ Good - handle errors
this.registerMarkdownCodeBlockProcessor('custom', (source, el, ctx) => {
try {
const result = this.processCustomSyntax(source);
el.setInnerHTML(result);
} catch (error) {
console.error('Processing error:', error);
el.setText('Error processing block');
el.addClass('processing-error');
}
});
// ❌ Bad - no error handling
this.registerMarkdownCodeBlockProcessor('custom', (source, el, ctx) => {
const result = this.processCustomSyntax(source); // May throw!
el.setInnerHTML(result);
});
Avoid Side Effects
// ✅ Good - pure processing
this.registerMarkdownPostProcessor((el, ctx) => {
const images = el.findAll('img');
images.forEach(img => img.addClass('lazy'));
});
// ❌ Bad - modifies global state
this.registerMarkdownPostProcessor((el, ctx) => {
this.totalImages++; // Side effect!
});
Clean Up Resources
// ✅ Good - async cleanup
this.registerMarkdownCodeBlockProcessor(
'diagram',
async (source, el, ctx) => {
const instance = await this.renderDiagram(source, el);
// Clean up when element is removed
this.register(() => {
instance.destroy();
});
}
);
Context Usage
// ✅ Good - use context
this.registerMarkdownPostProcessor((el, ctx) => {
// Conditional processing based on file
if (ctx.sourcePath.startsWith('daily/')) {
el.addClass('daily-note');
}
// Access frontmatter
if (ctx.frontmatter?.draft) {
el.addClass('draft-content');
}
});
Related
- Editor Extensions - Editor-level processing
- Plugin Class - Registration methods
- Best Practices - Development guidelines
