import { marked, type RendererThis, type Tokens } from "marked"; import DOMPurify from "dompurify"; import { linkifyTags, linkifyWikilinks } from "@/utils/tags"; function decodeEntities(text: string): string { const textarea = document.createElement("textarea"); textarea.innerHTML = text; return textarea.value; } export function slugify(text: string): string { return text .toLowerCase() .replace(/<[^>]+>/g, "") .replace(/[^\w\s-]/g, "") .trim() .replace(/\s+/g, "-"); } export function stripFirstLineTags(text: string): string { const match = text.match(/^[ \t]*((?:(? /^#[\w]+(?:\/[\w]+)*$/.test(t)); if (!allTags) return text; return text.slice(match[0].length); } const headingRenderer = { heading(this: RendererThis, { tokens, depth }: Tokens.Heading): string { const text = this.parser.parseInline(tokens); const id = slugify(text); return `${text}`; }, }; marked.use({ renderer: headingRenderer }); const PURIFY_OPTS_FULL = { ADD_ATTR: ["data-tag", "data-title"], FORCE_BODY: true, }; const PURIFY_OPTS_PREVIEW = { FORBID_TAGS: ["a", "img"], FORCE_BODY: true, }; export function renderMarkdown(text: string): string { const stripped = stripFirstLineTags(text); const decoded = decodeEntities(stripped); const html = marked(decoded) as string; const withTags = linkifyTags(html); const withLinks = linkifyWikilinks(withTags); const sanitized = DOMPurify.sanitize(withLinks, PURIFY_OPTS_FULL); // marked escapes ' to ' — replace after sanitization to ensure clean rendering return sanitized.replace(/'/g, "'"); } export function renderPreview(text: string): string { const stripped = stripFirstLineTags(text); const decoded = decodeEntities(stripped); const html = marked(decoded) as string; const withTags = linkifyTags(html); const withLinks = linkifyWikilinks(withTags); const sanitized = DOMPurify.sanitize(withLinks, PURIFY_OPTS_PREVIEW); return sanitized.replace(/'/g, "'"); }