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", "data-task-index"], FORCE_BODY: true, }; const PURIFY_OPTS_PREVIEW = { FORBID_TAGS: ["a", "img"], FORCE_BODY: true, }; export function renderMarkdown(text: string, { interactiveCheckboxes = false } = {}): string { const stripped = stripFirstLineTags(text); const decoded = decodeEntities(stripped); let html = marked(decoded) as string; if (interactiveCheckboxes) { // marked renders task-list checkboxes as . // Remove disabled and stamp a sequential data-task-index so the viewer // can identify which item was toggled regardless of attribute order. html = html.replace(/ disabled=""/g, ""); let taskIdx = 0; html = html.replace(/]*type="checkbox"[^>]*)>/g, (_m, attrs) => { return ``; }); } 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, "'"); }