import { marked } from "marked"; import sanitizeHtml from "sanitize-html"; marked.setOptions({ gfm: true, breaks: false }); /** Markdown → sanitized HTML (descriptions are editorial but still pass through a strict allow-list). */ export function renderMarkdown(md: string): string { const raw = marked.parse(md, { async: false }) as string; return sanitizeHtml(raw, { allowedTags: ["p", "em", "strong", "code", "pre", "ul", "ol", "li", "a", "h3", "h4", "blockquote", "br", "table", "thead", "tbody", "tr", "th", "td"], allowedAttributes: { a: ["href", "rel", "target"] }, allowedSchemes: ["https", "http", "mailto"], transformTags: { a: (tag, attribs) => ({ tagName: "a", attribs: { ...attribs, rel: "noopener noreferrer nofollow", target: "_blank" } }) }, }); } export const slugify = (s: string) => s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80); export const titleCase = (s: string) => s.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); export const humanize = (s: string) => s.replace(/[-_]/g, " ").replace(/^\w/, (c) => c.toUpperCase()); export function excerpt(md: string, n = 160) { const t = md.replace(/[#*_`>\[\]()]/g, "").replace(/\s+/g, " ").trim(); return t.length > n ? t.slice(0, n - 1).trimEnd() + "…" : t; }