Files
miti99/.github/scripts/lib/render-projects.mjs
T
tiennm99 4656fbed46 feat(projects): split projects page into starred, linked and contributed lists
Resolve owned namespaces from org memberships with the admin role instead of
assuming a fixed owner list, so repos under orgs I own are included.

- starred: owned public non-fork repos with at least one star, by stars then
  most recent push
- linked: owned public non-fork repos exposing a homepage or Pages URL, by most
  recent push
- contributed: public repos owned by others, by my merged PR count then most
  recent push

Private repos are excluded everywhere; forks are excluded from the owned lists
because their homepage belongs to upstream. Split the generator into API access
and rendering modules.
2026-09-18 12:24:44 +07:00

110 lines
3.0 KiB
JavaScript

// Markdown rendering and marker injection for the projects page.
/**
* Public URL of a repo: its declared homepage, else its GitHub Pages URL when
* Pages is enabled. Returns an empty string when the repo has neither.
* @param {import("./github-api.mjs").Repo} repo
* @returns {string}
*/
export function resolveUrl(repo) {
const home = (repo.homepage || "").trim();
if (home) return home;
if (repo.hasPages) return `https://${repo.owner}.github.io/${repo.name}/`;
return "";
}
/**
* @param {string} text
* @returns {string} Cell-safe single-line text.
*/
function cell(text) {
return (text || "")
.replace(/\|/g, "\\|")
.replace(/\r?\n/g, " ")
.trim();
}
/**
* @param {string} url
* @returns {string}
*/
function linkOrDash(url) {
return url ? `[${url}](${url})` : "—";
}
/**
* @param {string[]} headers
* @param {string[][]} rows
* @returns {string[]} Markdown table lines, or an empty-state line.
*/
function table(headers, rows) {
if (rows.length === 0) return ["_Chưa có project nào._"];
return [
`| ${headers.join(" | ")} |`,
`| ${headers.map(() => "---").join(" | ")} |`,
...rows.map((r) => `| ${r.join(" | ")} |`),
];
}
/**
* Repos with at least one star, most-starred first.
* @param {import("./github-api.mjs").Repo[]} repos
* @returns {string[]}
*/
export function renderStarred(repos) {
const rows = repos.map((r) => [
`[${r.nameWithOwner}](${r.htmlUrl})`,
cell(r.description),
String(r.stars),
linkOrDash(resolveUrl(r)),
]);
return table(["Name", "Description", "Stars", "URL"], rows);
}
/**
* Repos that have a clickable URL, by name.
* @param {import("./github-api.mjs").Repo[]} repos
* @returns {string[]}
*/
export function renderLinked(repos) {
const rows = repos.map((r) => [
`[${r.nameWithOwner}](${r.htmlUrl})`,
cell(r.description),
linkOrDash(resolveUrl(r)),
]);
return table(["Name", "Description", "URL"], rows);
}
/**
* Repos owned by others, by merged pull request count.
* @param {import("./github-api.mjs").Contribution[]} contributions
* @returns {string[]}
*/
export function renderContributed(contributions) {
const rows = contributions.map((c) => [
`[${c.repo.nameWithOwner}](${c.repo.htmlUrl})`,
cell(c.repo.description),
String(c.mergedPrs),
String(c.repo.stars),
]);
return table(["Name", "Description", "Merged PRs", "Stars"], rows);
}
/**
* Replace the content between a marker pair. Throws when the pair is missing,
* so a renamed marker fails loudly instead of silently dropping a list.
* @param {string} source
* @param {string} marker Marker base name, e.g. `PROJECTS_STARRED`.
* @param {string[]} lines
* @returns {string}
*/
export function injectBlock(source, marker, lines) {
const start = `<!-- ${marker}_START -->`;
const end = `<!-- ${marker}_END -->`;
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
if (!pattern.test(source)) {
throw new Error(`Marker pair ${marker} not found in projects page`);
}
return source.replace(pattern, [start, "", ...lines, "", end].join("\n"));
}