1234import MarkdownIt from "markdown-it";56type Style = "bang" | "slash" | "jvm" | "hash" | "python" | "markdown";78const STYLES: Record<string, Style> = {9 zig: "bang",10 rs: "bang",11 py: "python",12 md: "markdown",13 markdown: "markdown",14};15for (const ext of "v go c h cc cpp cxx hpp hh js mjs cjs jsx ts tsx mts cts".split(" ")) {16 STYLES[ext] = "slash";17}18for (const ext of "kt kts java swift".split(" ")) STYLES[ext] = "jvm";19for (const ext of "sh bash zsh nix toml yaml yml".split(" ")) STYLES[ext] = "hash";2021const SHELL_SHEBANG = /^#!.*\b(ba|z|da|k)?sh\b/;22const MODULE_FILES = ["mod.zig", "mod.rs", "index.ts", "__init__.py"];23const README = /^readme\.(md|markdown)$/i;2425262728293031const LICENSE = new RegExp(32 String.raw`SPDX-License-Identifier|\bcopyright\s*(\(c\)|©|\d{4})|©\s*\d{4}|` +33 String.raw`\blicensed under\b|\ball rights reserved\b`,34 "i",35);3637383940const PREAMBLE = /^(package|import)\s|^@file:/;4142const DIRECTIVE = /^\/\/(go:|\s*\+build\b)/;434445const markdown = new MarkdownIt({ html: true });464748export function describeFile(path: string, text: string): string | null {49 const name = path.split("/").pop() ?? path;50 const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);51 const ext = name.includes(".") ? (name.split(".").pop() as string).toLowerCase() : "";52 const style = STYLES[ext] ?? (ext === "" && SHELL_SHEBANG.test(lines[0] ?? "") ? "hash" : null);53 if (style === "bang") return bangLines(lines);54 if (style === "slash") return slashBlock(lines);55 if (style === "jvm") return slashBlock(lines.filter((line) => !PREAMBLE.test(line.trim())));56 if (style === "hash") return hashBlock(lines);57 if (style === "python") return docstring(lines);58 if (style === "markdown") return firstParagraph(text);59 return null;60}616263export function describeFolder(64 children: { name: string; description: string | null }[],65): string | null {66 const readme = children.find((child) => README.test(child.name))?.description;67 const moduleFile = MODULE_FILES.map((name) => children.find((c) => c.name === name)).find(68 Boolean,69 );70 return readme ?? moduleFile?.description ?? null;71}727374export function firstSentence(description: string): string {75 const paragraph = (description.trim().split(/\n\s*\n/)[0] as string).replace(/\s*\n\s*/g, " ");76 let inCode = false;77 for (let i = 0; i < paragraph.length; i++) {78 const c = paragraph[i] as string;79 if (c === "`") inCode = !inCode;80 const ends = i + 1 === paragraph.length || /\s/.test(paragraph[i + 1] as string);81 if (!inCode && ".!?".includes(c) && ends && !ABBREVIATION.test(paragraph.slice(0, i + 1))) {82 return paragraph.slice(0, i + 1);83 }84 }85 return paragraph;86}8788const ABBREVIATION = /\b(e\.g|i\.e|etc|vs|cf)\.$/i;8990function joined(lines: string[]): string | null {91 const text = lines.join("\n").trim();92 return text === "" ? null : text;93}949596function bangLines(lines: string[]): string | null {97 const body: string[] = [];98 for (const raw of lines) {99 const line = raw.trim();100 if (line.startsWith("//!")) body.push(line.replace(/^\/\/! ?/, ""));101 else if (body.length > 0 || !(line === "" || line.startsWith("//"))) break;102 }103 return joined(body);104}105106function slashBlock(lines: string[]): string | null {107 const block = commentBlocks(lines, (line) => line.startsWith("//"), true).find(108 (b) => !b.lines.every((line) => DIRECTIVE.test(line)),109 );110 if (!block) return null;111 if (!block.starred) return joined(block.lines.map((line) => line.replace(/^\/\/[/!]? ?/, "")));112 return joined(113 block.lines.map((line) => line.replace(/\s*\*\/$/, "").replace(/^(\/\*[*!]?|\*(?!\/)) ?/, "")),114 );115}116117118function hashBlock(lines: string[]): string | null {119 const rest = lines[0]?.startsWith("#!") ? lines.slice(1) : lines;120 const block = commentBlocks(rest, (line) => line.startsWith("#"))[0];121 return block ? joined(block.lines.map((line) => line.replace(/^#+ ?/, ""))) : null;122}123124type Block = { lines: string[]; starred: boolean };125126127128129130function commentBlocks(131 lines: string[],132 isComment: (trimmed: string) => boolean,133 starred = false,134): Block[] {135 const blocks: Block[] = [];136 let i = 0;137 while (i < lines.length) {138 const line = (lines[i] as string).trim();139 if (line === "") {140 i++;141 } else if (starred && line.startsWith("/*")) {142 const start = i;143 while (i < lines.length && !(lines[i] as string).includes("*/")) i++;144 if (i === lines.length) break;145 const body = lines.slice(start, ++i).map((l) => l.trim());146 const last = body.length - 1;147 148 body[last] = (body[last] as string).slice(0, (body[last] as string).indexOf("*/") + 2);149 blocks.push({ lines: body, starred: true });150 if (!(lines[i - 1] as string).trim().endsWith("*/")) break;151 } else if (isComment(line)) {152 const body: string[] = [];153 while (i < lines.length && isComment((lines[i] as string).trim())) {154 body.push((lines[i++] as string).trim());155 }156 blocks.push({ lines: body, starred: false });157 } else {158 break;159 }160 }161 if (blocks[0] && LICENSE.test(blocks[0].lines.join("\n"))) blocks.shift();162 return blocks;163}164165166function docstring(lines: string[]): string | null {167 let i = 0;168 while (i < lines.length && /^\s*(#.*)?$/.test(lines[i] as string)) i++;169 const open = /^[rRuU]?("""|'''|"|')/.exec(lines[i] ?? "");170 if (!open) return null;171 const quote = open[1] as string;172 const first = (lines[i] as string).slice(open[0].length);173 const body: string[] = [];174 for (let line = first, n = i; n < lines.length; line = lines[++n] ?? "") {175 const close = line.indexOf(quote);176 if (close >= 0) {177 body.push(line.slice(0, close));178 return joined(dedent(body));179 }180 if (quote.length === 1) return null;181 body.push(line);182 }183 return null;184}185186187function dedent(lines: string[]): string[] {188 const rest = lines.slice(1).filter((line) => line.trim() !== "");189 const indent = Math.min(...rest.map((line) => (/^\s*/.exec(line) as RegExpExecArray)[0].length));190 return [191 (lines[0] as string).trim(),192 ...lines.slice(1).map((line) => line.slice(Number.isFinite(indent) ? indent : 0)),193 ];194}195196function firstParagraph(text: string): string | null {197 const tokens = markdown.parse(text, {});198 const map = tokens.find((token) => token.type === "paragraph_open" && token.level === 0)?.map;199 return map ? joined(text.split(/\r?\n/).slice(map[0], map[1])) : null;200}