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("<!--", at);
const close = open < 0 ? -1 : 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;
};