SPB Git
1.3 KB · 19 lines typescript
1import { marked } from "marked";2import sanitizeHtml from "sanitize-html";34marked.setOptions({ gfm: true, breaks: false });5/** Markdown → sanitized HTML (descriptions are editorial but still pass through a strict allow-list). */6export function renderMarkdown(md: string): string {7  const raw = marked.parse(md, { async: false }) as string;8  return sanitizeHtml(raw, {9    allowedTags: ["p", "em", "strong", "code", "pre", "ul", "ol", "li", "a", "h3", "h4", "blockquote", "br", "table", "thead", "tbody", "tr", "th", "td"],10    allowedAttributes: { a: ["href", "rel", "target"] },11    allowedSchemes: ["https", "http", "mailto"],12    transformTags: { a: (tag, attribs) => ({ tagName: "a", attribs: { ...attribs, rel: "noopener noreferrer nofollow", target: "_blank" } }) },13  });14}15export const slugify = (s: string) => s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);16export const titleCase = (s: string) => s.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());17export const humanize = (s: string) => s.replace(/[-_]/g, " ").replace(/^\w/, (c) => c.toUpperCase());18export 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; }19
Files · laptop-initial-20261102
Menu

Theme