LoopKitGuides
Custom Content Transforms
Build custom content transforms for the rendering pipeline
Overview
Content transforms process card HTML after template interpolation. They run sequentially in the pipeline and can modify, enrich, or sanitize the content.
The Interface
type ContentTransform = (html: string, context: RenderContext) => string;
interface RenderContext {
fields: Record<string, string>; // Note field values
cardState: CardState; // 'new' | 'learning' | 'review' | 'relearning'
}A transform receives the current HTML and render context, and returns the modified HTML.
Example: Emoji Shortcode Transform
Replace text shortcodes with emoji:
import type { ContentTransform } from '@loopkit/nestjs';
const emojiMap: Record<string, string> = {
':check:': '\u2705',
':cross:': '\u274C',
':star:': '\u2B50',
':warning:': '\u26A0\uFE0F',
':info:': '\u2139\uFE0F',
};
export const emojiTransform: ContentTransform = (html) => {
let result = html;
for (const [code, emoji] of Object.entries(emojiMap)) {
result = result.replaceAll(code, emoji);
}
return result;
};Example: Auto-Link Transform
Automatically convert URLs to clickable links:
const autoLinkTransform: ContentTransform = (html) => {
return html.replace(
/(https?:\/\/[^\s<]+)/g,
'<a href="$1" target="_blank" rel="noopener">$1</a>',
);
};Example: Context-Aware Transform
Use the render context to modify behavior:
const hintTransform: ContentTransform = (html, context) => {
// Only show hints for new cards
if (context.cardState !== 'new') {
return html.replace(/<span class="hint">.*?<\/span>/g, '');
}
return html;
};Composing Transforms
Pass your transforms to createContentPipeline():
import {
createContentPipeline,
createMarkdownTransform,
createSanitizeTransform,
} from '@loopkit/nestjs';
import { parse } from 'marked';
import sanitize from 'sanitize-html';
const pipeline = createContentPipeline([
createMarkdownTransform({ parse }), // 1. Markdown → HTML
emojiTransform, // 2. Emoji shortcodes
autoLinkTransform, // 3. Auto-link URLs
createSanitizeTransform(sanitize), // 4. Sanitize (always last!)
]);Transform Ordering
Transforms run in array order. Recommended ordering:
- Markdown — Convert Markdown to HTML first
- Math (KaTeX) — Render math expressions
- Code highlighting — Highlight code blocks
- Custom transforms — Your application-specific transforms
- Sanitize — Always last to clean up any XSS vectors
Register the Pipeline
import { LoopKitModule, PrismaLoopKitAdapter } from '@loopkit/nestjs';
@Module({
imports: [
LoopKitModule.register({
storage: new PrismaLoopKitAdapter(prismaClient),
contentPipeline: pipeline,
}),
],
})
export class AppModule {}Accessing the Pipeline
Inject the pipeline for standalone use:
import { Inject, Injectable } from '@nestjs/common';
import { CONTENT_PIPELINE } from '@loopkit/nestjs';
import type { ContentPipeline } from '@loopkit/nestjs';
@Injectable()
export class PreviewService {
constructor(
@Inject(CONTENT_PIPELINE) private readonly pipeline: ContentPipeline,
) {}
previewCard(note: NoteBase, template: TemplateDef): RenderedCard {
return this.pipeline.render(note, template);
}
}