syntaxai/tdd.md · main · src / b51_render_repo.ts
// c51 — UI: tree listing + blob viewer for the local bare repo.
// Visited at /GIT/:repo/tree/:ref/<path> and /blob/:ref/<path>.
// Renders through tdd.md's chrome (renderPage with bodyHtml). Markdown
// blobs get parsed via marked; everything else is rendered as
// preformatted source.
import { marked } from "marked";
import { renderPage, escape } from "./b51_render_layout.ts";
import type { TreeEntry } from "./a31_git_parse.ts";
const shortSha = (sha: string): string => sha.slice(0, 7);
// Build a breadcrumb: "owner/repo · main · content/blog" with each
// segment a clickable link to /GIT/.../tree/<ref>/<segments-so-far>.
const renderBreadcrumb = (params: {
owner: string;
repo: string;
ref: string;
path: string;
asBlob?: boolean;
}): string => {
const { owner, repo, ref, path, asBlob } = params;
const repoLink = `<a href="/GIT/${escape(repo)}/tree/${escape(ref)}"><strong>${escape(owner)}/${escape(repo)}</strong></a>`;
const refLink = `<a class="commit-meta-pill" href="/GIT/${escape(repo)}/tree/${escape(ref)}"><code>${escape(ref)}</code></a>`;
if (path === "") return `<p class="commit-breadcrumb">${repoLink} · ${refLink}</p>`;
const segments = path.split("/");
const lastIdx = segments.length - 1;
const links = segments
.map((seg, i) => {
const so_far = segments.slice(0, i + 1).join("/");
// For blob view, the last segment is the file itself — no link.
// For tree view, every segment links to the tree at that depth.
const isLastFile = asBlob && i === lastIdx;
if (isLastFile) return `<code>${escape(seg)}</code>`;
return `<a href="/GIT/${escape(repo)}/tree/${escape(ref)}/${escape(so_far)}"><code>${escape(seg)}</code></a>`;
})
.join(" / ");
return `<p class="commit-breadcrumb">${repoLink} · ${refLink} · ${links}</p>`;
};
// Sort: trees first, then blobs, alphabetically within each group.
// Mirrors what GitHub / Forgejo's tree views do.
const sortEntries = (entries: TreeEntry[]): TreeEntry[] => {
return [...entries].sort((a, b) => {
if (a.type !== b.type) return a.type === "tree" ? -1 : 1;
return a.name.localeCompare(b.name);
});
};
const renderTreeRow = (params: {
entry: TreeEntry;
owner: string;
repo: string;
ref: string;
parentPath: string;
}): string => {
const { entry, owner, repo, ref, parentPath } = params;
const childPath = parentPath === "" ? entry.name : `${parentPath}/${entry.name}`;
const icon =
entry.type === "tree" ? "📁" :
entry.type === "commit" ? "🔗" : // submodule
"📄";
const kind = entry.type === "tree" ? "tree" : "blob";
const href = `/GIT/${escape(repo)}/${kind}/${escape(ref)}/${escape(childPath)}`;
return `<tr class="repo-tree-row repo-tree-row-${entry.type}">
<td class="repo-tree-icon">${icon}</td>
<td class="repo-tree-name"><a href="${href}">${escape(entry.name)}</a></td>
<td class="repo-tree-sha"><code>${escape(shortSha(entry.sha))}</code></td>
</tr>`;
};
export const renderRepoTree = async (params: {
owner: string;
repo: string;
ref: string;
path: string;
entries: TreeEntry[];
}): Promise<string> => {
const { owner, repo, ref, path, entries } = params;
const sorted = sortEntries(entries);
const upRow = path === ""
? ""
: (() => {
const parentPath = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
const upHref = parentPath === ""
? `/GIT/${escape(repo)}/tree/${escape(ref)}`
: `/GIT/${escape(repo)}/tree/${escape(ref)}/${escape(parentPath)}`;
return `<tr class="repo-tree-row repo-tree-row-up"><td class="repo-tree-icon">⬆</td><td class="repo-tree-name"><a href="${upHref}">..</a></td><td></td></tr>`;
})();
const rows = entries.length === 0
? `<tr><td colspan="3" class="commit-empty">empty tree</td></tr>`
: upRow + sorted.map((entry) => renderTreeRow({ entry, owner, repo, ref, parentPath: path })).join("");
const titlePath = path === "" ? "" : ` · ${path}`;
const inner = `<main class="md commit-view">
${renderBreadcrumb({ owner, repo, ref, path })}
<h1 class="commit-subject">${escape(path === "" ? `${owner}/${repo}` : path)}</h1>
<p class="commit-files-summary">${entries.length} entr${entries.length === 1 ? "y" : "ies"} at <code>${escape(ref)}</code></p>
<table class="repo-tree-table"><tbody>${rows}</tbody></table>
</main>`;
return renderPage({
title: `${owner}/${repo}${titlePath} — tdd.md`,
bodyHtml: inner,
description: `Repository tree at ${ref}${path ? "/" + path : ""} on tdd.md.`,
noindex: true,
bodyClass: "commit-body-page",
hideNav: true,
});
};
const isMarkdown = (path: string): boolean => path.endsWith(".md");
export const renderRepoBlob = async (params: {
owner: string;
repo: string;
ref: string;
path: string;
content: string;
}): Promise<string> => {
const { owner, repo, ref, path, content } = params;
const filename = path.split("/").pop() ?? path;
// Markdown gets rendered through marked; code files get a <pre><code>
// block; everything else also <pre> (we don't try to syntax-highlight,
// just render readable monospace).
const bodyHtml = isMarkdown(path)
? `<div class="repo-blob-rendered md">${await marked.parse(content, { gfm: true, breaks: false })}</div>`
: `<pre class="repo-blob-source"><code>${escape(content)}</code></pre>`;
const inner = `<main class="md commit-view">
${renderBreadcrumb({ owner, repo, ref, path, asBlob: true })}
<header class="repo-blob-header">
<code class="repo-blob-path">${escape(filename)}</code>
<span class="repo-blob-meta">${content.split("\n").length} lines · ${content.length} bytes</span>
<span class="repo-blob-actions">
<a href="/GIT/${escape(repo)}/raw/${escape(ref)}/${escape(path)}">raw</a>
${isMarkdown(path) ? `· <a href="/GIT/${escape(repo)}/blob/${escape(ref)}/${escape(path)}?source=1">source</a>` : ""}
</span>
</header>
${bodyHtml}
</main>`;
return renderPage({
title: `${path} · ${owner}/${repo} — tdd.md`,
bodyHtml: inner,
description: `${path} at ${ref} on tdd.md.`,
noindex: true,
bodyClass: "commit-body-page",
hideNav: true,
});
};