mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
feat(cli): add backlink badge generation for guides
This commit is contained in:
1 parent
d3f7b1c6f9
commit
436ff3080e
1 file changed
+55
-6
+55
-6
@@ -21,11 +21,13 @@
|
|||||||
* - Every tag a page carries that no campaign topic teaches (not a key of
|
* - Every tag a page carries that no campaign topic teaches (not a key of
|
||||||
* `apps/docs/topic-landings.ts`) gets a placeholder guide page,
|
* `apps/docs/topic-landings.ts`) gets a placeholder guide page,
|
||||||
* `guides/<slug>.mdx`, with `list:` pointing at LeetCode's problem list for
|
* `guides/<slug>.mdx`, with `list:` pointing at LeetCode's problem list for
|
||||||
* the tag. Created once (a missing or empty file), then hand-owned; the tag's
|
* the tag. Created once (a missing or empty file), then hand-owned except
|
||||||
* badge on a problem page links to it. A guide may be filed into a category
|
* for `sidebar.badge`, which sync keeps at the guide's backlink count (the
|
||||||
* `(group)` folder (e.g. `guides/(2-algorithms)/`) — no route segment, so
|
* problem pages carrying the tag; dropped at zero); the tag's badge on a
|
||||||
|
* problem page links to it. A guide may be filed into a category `(group)`
|
||||||
|
* folder (e.g. `guides/(2-algorithms)/`) — no route segment, so
|
||||||
* `/guides/<slug>` holds — and is found there by slug; a new placeholder
|
* `/guides/<slug>` holds — and is found there by slug; a new placeholder
|
||||||
* lands at the `guides/` root for a human to file.
|
* lands at the `guides/` root for a human to file (and give an `icon:`).
|
||||||
*
|
*
|
||||||
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
|
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
|
||||||
* ordered by topic number via a script-owned `meta.ts`). A problem outside the
|
* ordered by topic number via a script-owned `meta.ts`). A problem outside the
|
||||||
@@ -418,6 +420,27 @@ function guidePage(tag: TopicTag): string {
|
|||||||
/** Folder meta for `guides/`: its own header tab (`navigation.tabs` in blume.config.ts), so only the label. */
|
/** Folder meta for `guides/`: its own header tab (`navigation.tabs` in blume.config.ts), so only the label. */
|
||||||
const GUIDES_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Guides" });\n`;
|
const GUIDES_META = `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: "Guides" });\n`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guide's frontmatter with `sidebar.badge` set to its backlink count — the
|
||||||
|
* one key sync owns on a hand-owned guide — or dropped (with a `sidebar:` left
|
||||||
|
* empty) at zero, so the sidebar shows a count only where there is something
|
||||||
|
* to count.
|
||||||
|
*/
|
||||||
|
function withBacklinkBadge(page: string, count: number): string {
|
||||||
|
const fm = page.match(/^---\n[\s\S]*?\n---/);
|
||||||
|
if (!fm) return page;
|
||||||
|
let head = fm[0].replace(/^ {2}badge: .*\n/m, "");
|
||||||
|
if (count > 0) {
|
||||||
|
const line = ` badge: '${count}'`;
|
||||||
|
head = /^sidebar:$/m.test(head)
|
||||||
|
? head.replace(/^sidebar:$/m, `sidebar:\n${line}`)
|
||||||
|
: head.replace(/\n---$/, `\nsidebar:\n${line}\n---`);
|
||||||
|
} else {
|
||||||
|
head = head.replace(/^sidebar:\n(?! {2})/m, "");
|
||||||
|
}
|
||||||
|
return head + page.slice(fm[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
// ── inline-code formatting heuristics ────────────────────────────
|
// ── inline-code formatting heuristics ────────────────────────────
|
||||||
|
|
||||||
/** Identifiers worth backticking in prose, harvested from the code. */
|
/** Identifiers worth backticking in prose, harvested from the code. */
|
||||||
@@ -662,6 +685,7 @@ const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
|
|||||||
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
||||||
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
|
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
|
||||||
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
|
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
|
||||||
|
const tagged = new Map<string, number>(); // tag slug -> how many problem pages carry it
|
||||||
|
|
||||||
for (const sol of ordered) {
|
for (const sol of ordered) {
|
||||||
const langs = Object.keys(sol.code).sort().join("+");
|
const langs = Object.keys(sol.code).sort().join("+");
|
||||||
@@ -677,6 +701,7 @@ for (const sol of ordered) {
|
|||||||
const existing = pages.get(sol.num);
|
const existing = pages.get(sol.num);
|
||||||
await mkdir(join(DOCS, dir), { recursive: true });
|
await mkdir(join(DOCS, dir), { recursive: true });
|
||||||
|
|
||||||
|
let written: string; // the page as it stands after this run, for its topics
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const page = await Bun.file(join(DOCS, existing)).text();
|
const page = await Bun.file(join(DOCS, existing)).text();
|
||||||
const at = page.indexOf("## Solution");
|
const at = page.indexOf("## Solution");
|
||||||
@@ -698,12 +723,16 @@ for (const sol of ordered) {
|
|||||||
actions.push("updated");
|
actions.push("updated");
|
||||||
}
|
}
|
||||||
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
||||||
for (const tag of frontmatterTopics(next)) tags.set(tag.slug, tag);
|
written = next;
|
||||||
} else {
|
} else {
|
||||||
const page = renderPage(sol, await fetchTopicTags(sol));
|
const page = renderPage(sol, await fetchTopicTags(sol));
|
||||||
await Bun.write(join(DOCS, name), page);
|
await Bun.write(join(DOCS, name), page);
|
||||||
report.push([workRef, name, "created"]);
|
report.push([workRef, name, "created"]);
|
||||||
for (const tag of frontmatterTopics(page)) tags.set(tag.slug, tag);
|
written = page;
|
||||||
|
}
|
||||||
|
for (const tag of frontmatterTopics(written)) {
|
||||||
|
tags.set(tag.slug, tag);
|
||||||
|
tagged.set(tag.slug, (tagged.get(tag.slug) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
// The route drops the phase `(group)` and the page's number prefix.
|
// The route drops the phase `(group)` and the page's number prefix.
|
||||||
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
|
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
|
||||||
@@ -743,6 +772,26 @@ for (const tag of guideTags) {
|
|||||||
report.push([`tag ${tag.slug}`, rel, "created"]);
|
report.push([`tag ${tag.slug}`, rel, "created"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every guide's sidebar badge is its backlink count: the problem pages whose
|
||||||
|
// tag badge links to it, which is every page carrying the tag, since a guide's
|
||||||
|
// tag has no landing (`tagTarget()`). A guide whose tag a landing has since
|
||||||
|
// claimed has no backlinks left. Counts move with every solved problem, so the
|
||||||
|
// badge is rewritten whenever it differs; the rest of the guide is untouched.
|
||||||
|
// The same pass warns when a guide's hand-picked `icon:` is one a topic group
|
||||||
|
// owns (`TOPIC_ICONS`), so a guide row never reads as a topic row.
|
||||||
|
const topicIcons = new Set(Object.values(TOPIC_ICONS));
|
||||||
|
for await (const rel of new Bun.Glob("guides/**/*.mdx").scan(DOCS)) {
|
||||||
|
const slug = basename(rel, ".mdx");
|
||||||
|
const count = TOPIC_LANDINGS[slug] ? 0 : (tagged.get(slug) ?? 0);
|
||||||
|
const page = await Bun.file(join(DOCS, rel)).text();
|
||||||
|
const icon = page.match(/^---\n[\s\S]*?\n---/)?.[0].match(/^icon: (\S+)$/m)?.[1];
|
||||||
|
if (icon && topicIcons.has(icon)) console.warn(`${rel}: icon ${JSON.stringify(icon)} is a topic's (TOPIC_ICONS); pick another`);
|
||||||
|
const next = withBacklinkBadge(page, count);
|
||||||
|
if (next === page) continue;
|
||||||
|
await Bun.write(join(DOCS, rel), next);
|
||||||
|
report.push([`${count} backlinks`, rel, "badged"]);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase metas: created when missing (an empty file counts as missing), never
|
// Phase metas: created when missing (an empty file counts as missing), never
|
||||||
// rewritten, so a renamed phase title survives every sync.
|
// rewritten, so a renamed phase title survives every sync.
|
||||||
for (const [dir, phase] of phases) {
|
for (const [dir, phase] of phases) {
|
||||||
|
|||||||
Reference in new issue
Block a user