My App
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;
};

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:

  1. Markdown — Convert Markdown to HTML first
  2. Math (KaTeX) — Render math expressions
  3. Code highlighting — Highlight code blocks
  4. Custom transforms — Your application-specific transforms
  5. 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);
    }
}

On this page