Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

260
scripts/lib/doc-tree.ts Normal file
View File

@@ -0,0 +1,260 @@
/**
* Shared helpers for the documentation-tree scripts:
* - `firstBodyParagraph` — extract a truncated first paragraph from a `.md`.
* - `walkMd` — recursive `.md` walker honoring an ignore list.
* - `replaceMarkedSection` — idempotent in-place section replacement.
* - `extractRelativeMdLinks` — pull `.md` link targets out of markdown content.
* - `findCanonicalMd` — locate the dominant `.md` for a directory.
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, relative } from "node:path";
export const AUTO_MARKER_BEGIN = "<!-- begin:auto-docs -->";
export const AUTO_MARKER_END = "<!-- end:auto-docs -->";
/**
* Strip a leading `# Header` line and any frontmatter, then return the first
* non-empty paragraph (text up to the next blank line). Inline markdown
* formatting is preserved; whitespace is collapsed; the result is truncated
* to `maxChars` characters (with a trailing ellipsis when it overflows).
*/
export function firstBodyParagraph(content: string, maxChars = 120): string {
let body = content.replace(/^/, "");
if (body.startsWith("---\n")) {
const end = body.indexOf("\n---", 4);
if (end !== -1) body = body.slice(end + 4);
}
const lines = body.split("\n");
let i = 0;
// Skip leading whitespace + every heading line so the first prose
// paragraph is grabbed regardless of whether the doc starts with
// `# Title\n## Purpose\n\nprose…` or `# Title\n\nprose…`.
const isHeading = (line: string): boolean => /^#{1,6}\s/.test(line);
while (i < lines.length) {
const line = (lines[i] ?? "").trim();
if (line === "") {
i++;
continue;
}
if (isHeading(lines[i] ?? "")) {
i++;
continue;
}
break;
}
const paragraph: string[] = [];
while (i < lines.length) {
const line = lines[i] ?? "";
if (line.trim() === "") break;
if (/^(#{1,6}\s|```|---|>\s)/.test(line)) break;
paragraph.push(line);
i++;
}
// Strip inline markdown link syntax — `[text](url)` becomes `text` —
// so the truncation can never land inside a URL or break MDX autolink
// parsing downstream. Bare `<https://...>` autolinks are stripped to
// their text form too. Descriptions are display-only; the clickable
// links live elsewhere in the doc tree.
const stripped = paragraph
.join(" ")
.replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
.replace(/<(https?:\/\/[^>]+)>/gu, "$1")
.replace(/\s+/g, " ")
.trim();
if (!stripped) return "";
if (stripped.length <= maxChars) return stripped;
let cut = stripped.slice(0, maxChars - 1).trimEnd();
// Belt-and-braces: if anything URL-shaped still extends past the cut,
// walk back to the last whitespace before it so the truncated string
// never trails a partial URL.
const lastHttpIdx = Math.max(
cut.lastIndexOf("https://"),
cut.lastIndexOf("http://")
);
if (lastHttpIdx >= 0 && !/\s/.test(cut.slice(lastHttpIdx))) {
cut = cut.slice(0, lastHttpIdx).trimEnd();
}
return `${cut}…`;
}
export interface WalkOptions {
/** Directory names (segments) to skip entirely. */
ignoreDirs?: readonly string[];
/** File names to skip. */
ignoreFiles?: readonly string[];
/** Path segments — if any segment matches, the file is skipped. */
ignoreSegments?: readonly string[];
}
const DEFAULT_IGNORE_DIRS: readonly string[] = [
"node_modules",
"dist",
"build",
"coverage",
".turbo",
".vite",
".next",
];
export function walkMd(root: string, options: WalkOptions = {}): string[] {
const ignoreDirs = new Set([
...DEFAULT_IGNORE_DIRS,
...(options.ignoreDirs ?? []),
]);
const ignoreFiles = new Set(options.ignoreFiles ?? []);
const ignoreSegments = options.ignoreSegments ?? [];
const out: string[] = [];
const stack: string[] = [root];
while (stack.length > 0) {
const dir = stack.pop() as string;
let entries: ReturnType<typeof readdirSync>;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const name = entry.name;
const full = join(dir, name);
if (entry.isDirectory()) {
if (ignoreDirs.has(name)) continue;
if (name.startsWith(".") && name !== ".") continue;
if (ignoreSegments.some((s) => full.includes(`${s}/`)))
continue;
stack.push(full);
} else if (entry.isFile()) {
if (!name.endsWith(".md")) continue;
if (ignoreFiles.has(name)) continue;
if (ignoreSegments.some((s) => full.includes(`${s}/`)))
continue;
out.push(full);
}
}
}
out.sort();
return out;
}
/**
* Find `<!-- begin:auto-docs -->...<!-- end:auto-docs -->` in `content` and
* replace the body between the markers with `replacement`. If markers are
* absent, the markered block is inserted before the first `## ` heading
* (so the autotable sits above hand-authored sections) or appended at the
* end of the file when no heading is found. Returns `{ next, changed }`.
*/
export function replaceMarkedSection(
content: string,
replacement: string
): { next: string; changed: boolean } {
const beginIdx = content.indexOf(AUTO_MARKER_BEGIN);
const endIdx = content.indexOf(AUTO_MARKER_END);
const block = `${AUTO_MARKER_BEGIN}\n${replacement.trim()}\n${AUTO_MARKER_END}`;
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
const before = content.slice(0, beginIdx);
let after = content.slice(endIdx + AUTO_MARKER_END.length);
// Strip any stacked trailing end markers (with optional surrounding
// whitespace) and truncated `<!-- en…` fragments — defensive cleanup
// against historical generator-output corruption.
after = after.replace(
/(?:[ \t]*<!-- end:auto-docs -->[ \t]*\n?)+/g,
""
);
after = after.replace(/[ \t]*<!--\s*en…[ \t]*\n?/g, "");
const next = `${before}${block}${after}`;
return { next, changed: next !== content };
}
const headingMatch = content.match(/^##\s.*$/m);
if (headingMatch?.index !== undefined) {
const insertAt = headingMatch.index;
const before = content.slice(0, insertAt);
const after = content.slice(insertAt);
const sep = before.endsWith("\n\n")
? ""
: before.endsWith("\n")
? "\n"
: "\n\n";
const trailing = after.startsWith("\n") ? "" : "\n\n";
const next = `${before}${sep}${block}${trailing}${after}`;
return { next, changed: true };
}
const trimmed = content.replace(/\s+$/u, "");
const next = `${trimmed}\n\n${block}\n`;
return { next, changed: next !== content };
}
/**
* Pull every relative `.md` link target out of markdown content. Returns
* absolute filesystem paths resolved against `sourcePath`'s directory.
* Non-relative links (`http(s)://`, anchors, absolute URLs) are skipped.
*/
export function extractRelativeMdLinks(
content: string,
sourcePath: string
): string[] {
const out: string[] = [];
const base = dirname(sourcePath);
const matches = content.matchAll(/\[(?:[^\]]*)\]\(([^)]+)\)/g);
for (const match of matches) {
const raw = (match[1] ?? "").split("#")[0]?.trim() ?? "";
if (raw?.endsWith(".md") && !/^(?:https?:|mailto:|\/)/.test(raw)) {
out.push(join(base, raw));
}
}
return out;
}
/**
* For a directory, return the canonical `.md` doc. Priority order:
* 1. `<dirName>.md` (matches directory name)
* 2. `README.md`
* 3. The single non-`.types.md` `.md` file if exactly one exists
* 4. `undefined`
*/
export function findCanonicalMd(
dir: string,
dirName: string
): string | undefined {
const candidates: string[] = [];
let entries: ReturnType<typeof readdirSync>;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return undefined;
}
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith(".md")) continue;
if (entry.name.endsWith(".types.md")) continue;
candidates.push(entry.name);
}
const exact = `${dirName}.md`;
if (candidates.includes(exact)) return join(dir, exact);
if (candidates.includes("README.md")) return join(dir, "README.md");
if (candidates.length === 1) return join(dir, candidates[0] ?? "");
return undefined;
}
export function isDirectorySafe(path: string): boolean {
try {
return statSync(path).isDirectory();
} catch {
return false;
}
}
export function tryRead(path: string): string | undefined {
try {
return readFileSync(path, "utf-8");
} catch {
return undefined;
}
}
export function relForLink(from: string, to: string): string {
const rel = relative(dirname(from), to).replace(/\\/g, "/");
return rel.startsWith(".") ? rel : `./${rel}`;
}