feat(knowledge): add markdown rendering (KNOW-004)

- Install marked, marked-highlight, marked-gfm-heading-id, sanitize-html
- Create markdown utility with GFM support (tables, task lists, strikethrough)
- Add code syntax highlighting with highlight.js
- Implement XSS sanitization for security
- Update knowledge service to use markdown renderer
- Add comprehensive test suite (34 tests, all passing)
- Generate IDs for headers for deep linking
- Cache rendered HTML in database for performance
This commit is contained in:
Jason Woltje
2026-01-29 16:57:57 -06:00
parent 4881d0698f
commit 287a0e2556
6 changed files with 839 additions and 12 deletions

View File

@@ -4,7 +4,6 @@ import {
ConflictException,
} from "@nestjs/common";
import { EntryStatus } from "@prisma/client";
import { marked } from "marked";
import slugify from "slugify";
import { PrismaService } from "../prisma/prisma.service";
import type { CreateEntryDto, UpdateEntryDto, EntryQueryDto } from "./dto";
@@ -12,20 +11,15 @@ import type {
KnowledgeEntryWithTags,
PaginatedEntries,
} from "./entities/knowledge-entry.entity";
import { renderMarkdown } from "./utils/markdown";
/**
* Service for managing knowledge entries
*/
@Injectable()
export class KnowledgeService {
constructor(private readonly prisma: PrismaService) {
// Configure marked for security and consistency
marked.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: false,
pedantic: false,
});
}
constructor(private readonly prisma: PrismaService) {}
/**
* Get all entries for a workspace (paginated and filterable)
@@ -175,8 +169,8 @@ export class KnowledgeService {
const baseSlug = this.generateSlug(createDto.title);
const slug = await this.ensureUniqueSlug(workspaceId, baseSlug);
// Render markdown to HTML
const contentHtml = await marked.parse(createDto.content);
// Render markdown to HTML with sanitization
const contentHtml = await renderMarkdown(createDto.content);
// Use transaction to ensure atomicity
const result = await this.prisma.$transaction(async (tx) => {
@@ -299,7 +293,7 @@ export class KnowledgeService {
// Render markdown if content is updated
let contentHtml = existing.contentHtml;
if (updateDto.content) {
contentHtml = await marked.parse(updateDto.content);
contentHtml = await renderMarkdown(updateDto.content);
}
// Build update data object conditionally