47 lines
1.6 KiB
TypeScript
47 lines
1.6 KiB
TypeScript
// Markdown inline syntax reduced to plain text.
|
|
function stripInline(text: string): string {
|
|
return text
|
|
.replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images
|
|
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // links → label
|
|
.replace(/[*_`~]+/g, "")
|
|
.replace(/nostr:([a-z0-9]{12})[a-z0-9]*/g, "@$1…") // keep a short handle
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function truncate(text: string, max: number): string {
|
|
return text.length > max ? text.slice(0, max - 1).trimEnd() + "…" : text;
|
|
}
|
|
|
|
function isProse(line: string): boolean {
|
|
return !!line && !line.startsWith("#") && !/^https?:\/\/\S+$/.test(line);
|
|
}
|
|
|
|
// Plain-text excerpt of a markdown body for description meta tags: the first
|
|
// paragraph that isn't a heading or a bare media URL, trimmed to `max` chars.
|
|
export function excerpt(markdown: string, max = 160): string {
|
|
const line = markdown
|
|
.split("\n")
|
|
.map((l) => l.trim())
|
|
.find(isProse);
|
|
return line ? truncate(stripInline(line), max) : "";
|
|
}
|
|
|
|
// The whole body flattened to one plain-text line, trimmed to `max` chars.
|
|
// Headings, bare media URLs, code blocks, quoted text and list markers are
|
|
// dropped, so a reply reads as its own words.
|
|
export function summarize(markdown: string, max = 200): string {
|
|
let inCode = false;
|
|
const lines: string[] = [];
|
|
for (const raw of markdown.split("\n")) {
|
|
const l = raw.trim();
|
|
if (l.startsWith("```")) {
|
|
inCode = !inCode;
|
|
continue;
|
|
}
|
|
if (inCode || l.startsWith(">")) continue;
|
|
const line = l.replace(/^(?:[-*+]\s+|\d+[.)]\s+)+/, "");
|
|
if (isProse(line)) lines.push(line);
|
|
}
|
|
return truncate(stripInline(lines.join(" ")), max);
|
|
}
|