import MarkdownIt from "markdown-it"; import type Token from "markdown-it/lib/token.mjs"; import { full as emoji } from "markdown-it-emoji"; const md = new MarkdownIt({ html: true }).use(emoji); const inlineText = (tokens: Token[]): string => tokens .map((token) => { switch (token.type) { case "text": case "code_inline": case "emoji": return token.content; case "image": return inlineText(token.children ?? []); case "softbreak": case "hardbreak": return " "; default: return ""; } }) .join(""); export const markdownToText = (source: string): string => md .parse(source, {}) .map((token) => { if (token.type === "inline") return inlineText(token.children ?? []); if (token.type === "fence" || token.type === "code_block") return token.content; return ""; }) .filter(Boolean) .join(" ") .replace(/\s+/g, " ") .trim(); // parsing a whole 256 KiB body costs about 12 ms of every page view const SUMMARY_SOURCE_CHARS = 4096; const LINK_DEFINITION = /^ {0,3}\[(?!\^)[^\]\n]+\]:[ \t]*\S.*$/gm; const withoutComments = (source: string): string => { let out = ""; let at = 0; for (;;) { const open = source.indexOf("", open + 4); if (close < 0) return out + source.slice(at); out += source.slice(at, open); at = close + 3; } }; export const summarize = (source: string, limit = 160): string => { const visible = withoutComments(source).replace(LINK_DEFINITION, ""); const text = markdownToText(visible.slice(0, SUMMARY_SOURCE_CHARS)); return text.length > limit || visible.length > SUMMARY_SOURCE_CHARS ? `${text.slice(0, limit - 1).trimEnd()}…` : text; };