mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
style(cli): reformat sync script for readability and consistency
This commit is contained in:
1 parent
6247915ae5
commit
cd5f416939
1 file changed
+166
-53
+166
-53
@@ -104,11 +104,15 @@ interface Solution {
|
|||||||
// ── work/ parsing ────────────────────────────────────────────────
|
// ── work/ parsing ────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Split a source file into (header comment text, code). */
|
/** Split a source file into (header comment text, code). */
|
||||||
function splitSource(src: string, lang: Lang): { header: string; code: string } {
|
function splitSource(
|
||||||
|
src: string,
|
||||||
|
lang: Lang,
|
||||||
|
): { header: string; code: string } {
|
||||||
let header: string, code: string;
|
let header: string, code: string;
|
||||||
if (lang === "js") {
|
if (lang === "js") {
|
||||||
const end = src.indexOf("*/");
|
const end = src.indexOf("*/");
|
||||||
if (!src.trimStart().startsWith("/*") || end === -1) throw new Error("missing /* header */");
|
if (!src.trimStart().startsWith("/*") || end === -1)
|
||||||
|
throw new Error("missing /* header */");
|
||||||
header = src
|
header = src
|
||||||
.slice(src.indexOf("/*") + 2, end)
|
.slice(src.indexOf("/*") + 2, end)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -140,7 +144,11 @@ function paragraphs(lines: string[]): string[] {
|
|||||||
flush();
|
flush();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (/^([•-] |Input:|Output:|Explanation:|Example \d+:|Constraints:|Follow[- ]?ups?:)/.test(text)) {
|
if (
|
||||||
|
/^([•-] |Input:|Output:|Explanation:|Example \d+:|Constraints:|Follow[- ]?ups?:)/.test(
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
) {
|
||||||
flush();
|
flush();
|
||||||
cur = text;
|
cur = text;
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +166,10 @@ function parseHeader(header: string): Header {
|
|||||||
const num = Number.parseInt(titleLine, 10);
|
const num = Number.parseInt(titleLine, 10);
|
||||||
const title = titleLine.replace(/^\d+\.\s*/, "");
|
const title = titleLine.replace(/^\d+\.\s*/, "");
|
||||||
const difficulty =
|
const difficulty =
|
||||||
lines.find((l) => l.trim().startsWith("Difficulty:"))?.split(":")[1]?.trim() ?? "";
|
lines
|
||||||
|
.find((l) => l.trim().startsWith("Difficulty:"))
|
||||||
|
?.split(":")[1]
|
||||||
|
?.trim() ?? "";
|
||||||
|
|
||||||
// Body: everything after the ───── separator.
|
// Body: everything after the ───── separator.
|
||||||
const sep = lines.findIndex((l) => /^[─]{3,}/.test(l.trim()));
|
const sep = lines.findIndex((l) => /^[─]{3,}/.test(l.trim()));
|
||||||
@@ -288,10 +299,18 @@ async function readmeTopics(): Promise<Map<number, Topic>> {
|
|||||||
const p = line.match(/^## Phase ([IVX]+) — (.+?) · /);
|
const p = line.match(/^## Phase ([IVX]+) — (.+?) · /);
|
||||||
if (p) {
|
if (p) {
|
||||||
const name = p[2]!.replaceAll("&", "&");
|
const name = p[2]!.replaceAll("&", "&");
|
||||||
phase = { order: ROMAN.indexOf(p[1]!) + 1, name: PHASE_ALIASES[name] ?? name };
|
phase = {
|
||||||
|
order: ROMAN.indexOf(p[1]!) + 1,
|
||||||
|
name: PHASE_ALIASES[name] ?? name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const t = line.match(/<strong>(\d+)\.\s*([^<]+)<\/strong>/);
|
const t = line.match(/<strong>(\d+)\.\s*([^<]+)<\/strong>/);
|
||||||
if (t) topic = { order: Number(t[1]), name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim(), phase };
|
if (t)
|
||||||
|
topic = {
|
||||||
|
order: Number(t[1]),
|
||||||
|
name: TOPIC_ALIASES[t[2]!.trim()] ?? t[2]!.trim(),
|
||||||
|
phase,
|
||||||
|
};
|
||||||
if (!topic) continue;
|
if (!topic) continue;
|
||||||
const n = line.match(/<td align="center">(\d+)<\/td>/);
|
const n = line.match(/<td align="center">(\d+)<\/td>/);
|
||||||
if (n && !map.has(Number(n[1]))) map.set(Number(n[1]), topic);
|
if (n && !map.has(Number(n[1]))) map.set(Number(n[1]), topic);
|
||||||
@@ -348,10 +367,16 @@ function phaseMeta(phase: Phase): string {
|
|||||||
*/
|
*/
|
||||||
function groupMeta(topic: Topic): string {
|
function groupMeta(topic: Topic): string {
|
||||||
const icon = TOPIC_ICONS[topic.name];
|
const icon = TOPIC_ICONS[topic.name];
|
||||||
if (!icon) console.warn(`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`);
|
if (!icon)
|
||||||
const fields = [`title: ${JSON.stringify(topic.name)}`, `order: ${topic.order ?? 0}`];
|
console.warn(
|
||||||
|
`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`,
|
||||||
|
);
|
||||||
|
const fields = [
|
||||||
|
`title: ${JSON.stringify(topic.name)}`,
|
||||||
|
`order: ${topic.order ?? 0}`,
|
||||||
|
];
|
||||||
if (icon) fields.push(`icon: ${JSON.stringify(icon)}`);
|
if (icon) fields.push(`icon: ${JSON.stringify(icon)}`);
|
||||||
fields.push(`display: "page"`);
|
fields.push(`display: "group"`);
|
||||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,17 +403,24 @@ async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | undefined> {
|
|||||||
referer: `https://leetcode.com/problems/${sol.slug}/`,
|
referer: `https://leetcode.com/problems/${sol.slug}/`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: "query topicTags($titleSlug: String!) { question(titleSlug: $titleSlug) { topicTags { name slug } } }",
|
query:
|
||||||
|
"query topicTags($titleSlug: String!) { question(titleSlug: $titleSlug) { topicTags { name slug } } }",
|
||||||
variables: { titleSlug: sol.slug },
|
variables: { titleSlug: sol.slug },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`POST /graphql -> ${res.status} ${res.statusText}`);
|
if (!res.ok)
|
||||||
const data = (await res.json()) as { data?: { question?: { topicTags?: TopicTag[] } | null } };
|
throw new Error(`POST /graphql -> ${res.status} ${res.statusText}`);
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
data?: { question?: { topicTags?: TopicTag[] } | null };
|
||||||
|
};
|
||||||
const tags = data.data?.question?.topicTags;
|
const tags = data.data?.question?.topicTags;
|
||||||
if (!Array.isArray(tags)) throw new Error("POST /graphql -> no such question");
|
if (!Array.isArray(tags))
|
||||||
|
throw new Error("POST /graphql -> no such question");
|
||||||
return tags.map(({ name, slug }) => ({ name, slug }));
|
return tags.map(({ name, slug }) => ({ name, slug }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`no topics for ${sol.num}.${sol.slug}: ${err instanceof Error ? err.message : err}`);
|
console.warn(
|
||||||
|
`no topics for ${sol.num}.${sol.slug}: ${err instanceof Error ? err.message : err}`,
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,15 +428,30 @@ async function fetchTopicTags(sol: Solution): Promise<TopicTag[] | undefined> {
|
|||||||
/** The frontmatter `topics:` block, one flow mapping per tag (JSON strings are valid YAML). */
|
/** The frontmatter `topics:` block, one flow mapping per tag (JSON strings are valid YAML). */
|
||||||
function topicsYaml(tags: TopicTag[]): string {
|
function topicsYaml(tags: TopicTag[]): string {
|
||||||
if (tags.length === 0) return "topics: []";
|
if (tags.length === 0) return "topics: []";
|
||||||
return ["topics:", ...tags.map((t) => ` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`)].join("\n");
|
return [
|
||||||
|
"topics:",
|
||||||
|
...tags.map(
|
||||||
|
(t) =>
|
||||||
|
` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`,
|
||||||
|
),
|
||||||
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The frontmatter's `topics:` as sync wrote them (or a human edited them); [] when absent. */
|
/** The frontmatter's `topics:` as sync wrote them (or a human edited them); [] when absent. */
|
||||||
function frontmatterTopics(page: string): TopicTag[] {
|
function frontmatterTopics(page: string): TopicTag[] {
|
||||||
const fm = page.match(/^---\n([\s\S]*?)\n---/)?.[1];
|
const fm = page.match(/^---\n([\s\S]*?)\n---/)?.[1];
|
||||||
const data: unknown = fm ? Bun.YAML.parse(fm) : undefined;
|
const data: unknown = fm ? Bun.YAML.parse(fm) : undefined;
|
||||||
if (!data || typeof data !== "object" || !("topics" in data) || !Array.isArray(data.topics)) return [];
|
if (
|
||||||
return data.topics.filter((t): t is TopicTag => typeof t?.name === "string" && typeof t?.slug === "string");
|
!data ||
|
||||||
|
typeof data !== "object" ||
|
||||||
|
!("topics" in data) ||
|
||||||
|
!Array.isArray(data.topics)
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
return data.topics.filter(
|
||||||
|
(t): t is TopicTag =>
|
||||||
|
typeof t?.name === "string" && typeof t?.slug === "string",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GitHub problem issues ────────────────────────────────────────
|
// ── GitHub problem issues ────────────────────────────────────────
|
||||||
@@ -422,7 +469,9 @@ function problemIssues(): Promise<Map<number, string> | undefined> {
|
|||||||
try {
|
try {
|
||||||
const gh = await github();
|
const gh = await github();
|
||||||
const urls = new Map<number, string>();
|
const urls = new Map<number, string>();
|
||||||
for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=problem&state=all`)) {
|
for await (const raw of gh.list(
|
||||||
|
`/repos/${gh.repo}/issues?labels=problem&state=all`,
|
||||||
|
)) {
|
||||||
if (!raw || typeof raw !== "object" || "pull_request" in raw) continue;
|
if (!raw || typeof raw !== "object" || "pull_request" in raw) continue;
|
||||||
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
||||||
if (!("html_url" in raw) || typeof raw.html_url !== "string") continue;
|
if (!("html_url" in raw) || typeof raw.html_url !== "string") continue;
|
||||||
@@ -431,7 +480,9 @@ function problemIssues(): Promise<Map<number, string> | undefined> {
|
|||||||
}
|
}
|
||||||
return urls;
|
return urls;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`no problem issues: ${err instanceof Error ? err.message : err}`);
|
console.warn(
|
||||||
|
`no problem issues: ${err instanceof Error ? err.message : err}`,
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -448,7 +499,10 @@ async function issueUrl(sol: Solution): Promise<string | undefined> {
|
|||||||
if (!topics.has(sol.num)) return undefined;
|
if (!topics.has(sol.num)) return undefined;
|
||||||
const urls = await problemIssues();
|
const urls = await problemIssues();
|
||||||
const url = urls?.get(sol.num);
|
const url = urls?.get(sol.num);
|
||||||
if (urls && !url) console.warn(`no problem issue titled "LC ${sol.num} · …" for ${sol.num}.${sol.slug}`);
|
if (urls && !url)
|
||||||
|
console.warn(
|
||||||
|
`no problem issue titled "LC ${sol.num} · …" for ${sol.num}.${sol.slug}`,
|
||||||
|
);
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,13 +512,15 @@ async function issueUrl(sol: Solution): Promise<string | undefined> {
|
|||||||
function identifiers(sol: Solution): Set<string> {
|
function identifiers(sol: Solution): Set<string> {
|
||||||
const ids = new Set<string>(["n", "m", "k"]);
|
const ids = new Set<string>(["n", "m", "k"]);
|
||||||
for (const code of Object.values(sol.code)) {
|
for (const code of Object.values(sol.code)) {
|
||||||
for (const m of code.matchAll(/@param\s*\{[^}]*\}\s*(\w+)/g)) ids.add(m[1]!);
|
for (const m of code.matchAll(/@param\s*\{[^}]*\}\s*(\w+)/g))
|
||||||
|
ids.add(m[1]!);
|
||||||
for (const m of code.matchAll(/def \w+\(self,?\s*([^)]*)\)/g))
|
for (const m of code.matchAll(/def \w+\(self,?\s*([^)]*)\)/g))
|
||||||
for (const arg of m[1]!.split(","))
|
for (const arg of m[1]!.split(","))
|
||||||
if (arg.trim()) ids.add(arg.split(":")[0]!.trim());
|
if (arg.trim()) ids.add(arg.split(":")[0]!.trim());
|
||||||
for (const m of code.matchAll(/(?:var|const|let) (\w+) = function\s*\(([^)]*)\)/g))
|
for (const m of code.matchAll(
|
||||||
for (const arg of m[2]!.split(","))
|
/(?:var|const|let) (\w+) = function\s*\(([^)]*)\)/g,
|
||||||
if (arg.trim()) ids.add(arg.trim());
|
))
|
||||||
|
for (const arg of m[2]!.split(",")) if (arg.trim()) ids.add(arg.trim());
|
||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
@@ -521,7 +577,11 @@ function backtickify(text: string, ids: Set<string>): string {
|
|||||||
while (j > i && /^[+\-*/=%]$/.test(toks[j]!.core)) j--;
|
while (j > i && /^[+\-*/=%]$/.test(toks[j]!.core)) j--;
|
||||||
const span = toks
|
const span = toks
|
||||||
.slice(i, j + 1)
|
.slice(i, j + 1)
|
||||||
.map((tok, idx, arr) => (idx === arr.length - 1 ? `${tok.pre}${tok.core}` : `${tok.pre}${tok.core}${tok.post}`))
|
.map((tok, idx, arr) =>
|
||||||
|
idx === arr.length - 1
|
||||||
|
? `${tok.pre}${tok.core}`
|
||||||
|
: `${tok.pre}${tok.core}${tok.post}`,
|
||||||
|
)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
out.push(`\`${span}\`${toks[j]!.post}`);
|
out.push(`\`${span}\`${toks[j]!.post}`);
|
||||||
i = j + 1;
|
i = j + 1;
|
||||||
@@ -549,14 +609,22 @@ function warningText(h: Header): string {
|
|||||||
return (
|
return (
|
||||||
h.statement
|
h.statement
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.find((p) => /^(You must|Your solution must|Your algorithm|Notice that|Could you)/.test(p)) ?? ""
|
.find((p) =>
|
||||||
|
/^(You must|Your solution must|Your algorithm|Notice that|Could you)/.test(
|
||||||
|
p,
|
||||||
|
),
|
||||||
|
) ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
|
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
|
||||||
const PAGE_ICON = "dot";
|
const PAGE_ICON = "dot";
|
||||||
|
|
||||||
function renderPage(sol: Solution, tags: TopicTag[] | undefined, issue: string | undefined): string {
|
function renderPage(
|
||||||
|
sol: Solution,
|
||||||
|
tags: TopicTag[] | undefined,
|
||||||
|
issue: string | undefined,
|
||||||
|
): string {
|
||||||
const h = sol.header;
|
const h = sol.header;
|
||||||
const ids = identifiers(sol);
|
const ids = identifiers(sol);
|
||||||
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
|
const description = h.statement[0]?.replace(/\.\s*$/, "") ?? "";
|
||||||
@@ -565,7 +633,9 @@ function renderPage(sol: Solution, tags: TopicTag[] | undefined, issue: string |
|
|||||||
parts.push(
|
parts.push(
|
||||||
"---",
|
"---",
|
||||||
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
|
`title: '${h.num}. ${h.title.replaceAll("'", "''")}'`,
|
||||||
/[:#]/.test(description) ? `description: '${description.replaceAll("'", "''")}'` : `description: ${description}`,
|
/[:#]/.test(description)
|
||||||
|
? `description: '${description.replaceAll("'", "''")}'`
|
||||||
|
: `description: ${description}`,
|
||||||
`icon: ${PAGE_ICON}`,
|
`icon: ${PAGE_ICON}`,
|
||||||
...(tags ? [topicsYaml(tags)] : []),
|
...(tags ? [topicsYaml(tags)] : []),
|
||||||
...(issue ? [`issue: ${JSON.stringify(issue)}`] : []),
|
...(issue ? [`issue: ${JSON.stringify(issue)}`] : []),
|
||||||
@@ -587,7 +657,8 @@ function renderPage(sol: Solution, tags: TopicTag[] | undefined, issue: string |
|
|||||||
parts.push(`- Explanation: ${backtickify(ex.explanation[0]!, ids)}`);
|
parts.push(`- Explanation: ${backtickify(ex.explanation[0]!, ids)}`);
|
||||||
else if (ex.explanation.length > 1) {
|
else if (ex.explanation.length > 1) {
|
||||||
parts.push("- Explanation:");
|
parts.push("- Explanation:");
|
||||||
for (const item of ex.explanation) parts.push(` - ${backtickify(item, ids)}`);
|
for (const item of ex.explanation)
|
||||||
|
parts.push(` - ${backtickify(item, ids)}`);
|
||||||
}
|
}
|
||||||
parts.push("");
|
parts.push("");
|
||||||
});
|
});
|
||||||
@@ -612,7 +683,10 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
|
|||||||
}
|
}
|
||||||
const num = Number(m[1]);
|
const num = Number(m[1]);
|
||||||
const slug = m[2]!;
|
const slug = m[2]!;
|
||||||
const { header, code } = splitSource(await Bun.file(join(WORK, rel)).text(), lang);
|
const { header, code } = splitSource(
|
||||||
|
await Bun.file(join(WORK, rel)).text(),
|
||||||
|
lang,
|
||||||
|
);
|
||||||
const parsed = parseHeader(header);
|
const parsed = parseHeader(header);
|
||||||
const existing = solutions.get(num);
|
const existing = solutions.get(num);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -661,7 +735,10 @@ async function ensureTopics(page: string, sol: Solution): Promise<string> {
|
|||||||
if (!fm || /^topics:/m.test(fm[0])) return page;
|
if (!fm || /^topics:/m.test(fm[0])) return page;
|
||||||
const tags = await fetchTopicTags(sol);
|
const tags = await fetchTopicTags(sol);
|
||||||
if (!tags) return page;
|
if (!tags) return page;
|
||||||
return fm[0].replace(/^sidebar:$/m, `${topicsYaml(tags)}\nsidebar:`) + page.slice(fm[0].length);
|
return (
|
||||||
|
fm[0].replace(/^sidebar:$/m, `${topicsYaml(tags)}\nsidebar:`) +
|
||||||
|
page.slice(fm[0].length)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert `issue:` before `sidebar:` when the frontmatter lacks it; an existing URL is kept as is. */
|
/** Insert `issue:` before `sidebar:` when the frontmatter lacks it; an existing URL is kept as is. */
|
||||||
@@ -670,7 +747,10 @@ async function ensureIssue(page: string, sol: Solution): Promise<string> {
|
|||||||
if (!fm || /^issue:/m.test(fm[0])) return page;
|
if (!fm || /^issue:/m.test(fm[0])) return page;
|
||||||
const url = await issueUrl(sol);
|
const url = await issueUrl(sol);
|
||||||
if (!url) return page;
|
if (!url) return page;
|
||||||
return fm[0].replace(/^sidebar:$/m, `issue: ${JSON.stringify(url)}\nsidebar:`) + page.slice(fm[0].length);
|
return (
|
||||||
|
fm[0].replace(/^sidebar:$/m, `issue: ${JSON.stringify(url)}\nsidebar:`) +
|
||||||
|
page.slice(fm[0].length)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -693,7 +773,9 @@ function solutionEnd(page: string, from: number): number {
|
|||||||
|
|
||||||
// Existing docs pages, keyed by leetcode number from the frontmatter title.
|
// Existing docs pages, keyed by leetcode number from the frontmatter title.
|
||||||
const pages = new Map<number, string>(); // num -> path relative to DOCS
|
const pages = new Map<number, string>(); // num -> path relative to DOCS
|
||||||
for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).sort()) {
|
for (const rel of (
|
||||||
|
await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))
|
||||||
|
).sort()) {
|
||||||
const text = await Bun.file(join(DOCS, rel)).text();
|
const text = await Bun.file(join(DOCS, rel)).text();
|
||||||
const t = text.match(/^title: '(\d+)\./m);
|
const t = text.match(/^title: '(\d+)\./m);
|
||||||
if (t) pages.set(Number(t[1]), rel);
|
if (t) pages.set(Number(t[1]), rel);
|
||||||
@@ -713,11 +795,12 @@ for (const sol of ordered) {
|
|||||||
// (0N-<phase>)/<topic>/(problems)/ folder (the topic is a real route segment,
|
// (0N-<phase>)/<topic>/(problems)/ folder (the topic is a real route segment,
|
||||||
// so its index.mdx is the landing page; `(problems)` adds none) +
|
// so its index.mdx is the landing page; `(problems)` adds none) +
|
||||||
// leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
|
// leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
|
||||||
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
|
const topic = topics.get(sol.num) ??
|
||||||
|
topicByName.get(sol.category) ?? { name: sol.category };
|
||||||
const dir = topicDir(topic);
|
const dir = topicDir(topic);
|
||||||
groups.set(dir, topic);
|
groups.set(dir, topic);
|
||||||
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
|
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
|
||||||
const name = `${dir}/(problems)/${sol.num}_${sol.slug}.mdx`;
|
const name = `${dir}/${sol.num}_${sol.slug}.mdx`;
|
||||||
const existing = pages.get(sol.num);
|
const existing = pages.get(sol.num);
|
||||||
await mkdir(join(DOCS, dirname(name)), { recursive: true });
|
await mkdir(join(DOCS, dirname(name)), { recursive: true });
|
||||||
|
|
||||||
@@ -730,8 +813,12 @@ for (const sol of ordered) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const tail = page.slice(solutionEnd(page, at));
|
const tail = page.slice(solutionEnd(page, at));
|
||||||
const body = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
const body =
|
||||||
const next = await ensureIssue(await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol), sol);
|
page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||||
|
const next = await ensureIssue(
|
||||||
|
await ensureTopics(ensureIcon(ensureLabel(body, sol.header)), sol),
|
||||||
|
sol,
|
||||||
|
);
|
||||||
const actions: string[] = [];
|
const actions: string[] = [];
|
||||||
if (existing !== name) {
|
if (existing !== name) {
|
||||||
await rename(join(DOCS, existing), join(DOCS, name));
|
await rename(join(DOCS, existing), join(DOCS, name));
|
||||||
@@ -745,14 +832,20 @@ for (const sol of ordered) {
|
|||||||
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
||||||
written = next;
|
written = next;
|
||||||
} else {
|
} else {
|
||||||
const page = renderPage(sol, await fetchTopicTags(sol), await issueUrl(sol));
|
const page = renderPage(
|
||||||
|
sol,
|
||||||
|
await fetchTopicTags(sol),
|
||||||
|
await issueUrl(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"]);
|
||||||
written = page;
|
written = page;
|
||||||
}
|
}
|
||||||
for (const tag of frontmatterTopics(written)) tags.set(tag.slug, tag);
|
for (const tag of frontmatterTopics(written)) tags.set(tag.slug, tag);
|
||||||
// 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}`)} },`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Script-owned short links, one per page; imported by blume.config.ts.
|
// Script-owned short links, one per page; imported by blume.config.ts.
|
||||||
@@ -765,9 +858,16 @@ for (const sol of ordered) {
|
|||||||
"];",
|
"];",
|
||||||
"",
|
"",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
if (!(await Bun.file(REDIRECTS).exists()) || (await Bun.file(REDIRECTS).text()) !== module) {
|
if (
|
||||||
|
!(await Bun.file(REDIRECTS).exists()) ||
|
||||||
|
(await Bun.file(REDIRECTS).text()) !== module
|
||||||
|
) {
|
||||||
await Bun.write(REDIRECTS, module);
|
await Bun.write(REDIRECTS, module);
|
||||||
report.push([`${redirects.length} redirects`, "apps/docs/redirects.ts", "written"]);
|
report.push([
|
||||||
|
`${redirects.length} redirects`,
|
||||||
|
"apps/docs/redirects.ts",
|
||||||
|
"written",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,16 +876,29 @@ for (const sol of ordered) {
|
|||||||
// term's icon is also the icon on every problem-page badge linking to its
|
// term's icon is also the icon on every problem-page badge linking to its
|
||||||
// card, so it must not be a topic group's (`TOPIC_ICONS`), so a term badge
|
// card, so it must not be a topic group's (`TOPIC_ICONS`), so a term badge
|
||||||
// never reads as a topic's, nor another term's.
|
// never reads as a topic's, nor another term's.
|
||||||
for (const tag of [...tags.values()].sort((a, b) => a.slug.localeCompare(b.slug))) {
|
for (const tag of [...tags.values()].sort((a, b) =>
|
||||||
|
a.slug.localeCompare(b.slug),
|
||||||
|
)) {
|
||||||
if (!TOPIC_LANDINGS[tag.slug] && !APPENDIX[tag.slug])
|
if (!TOPIC_LANDINGS[tag.slug] && !APPENDIX[tag.slug])
|
||||||
console.warn(`tag ${tag.slug}: no Appendix term; add ${JSON.stringify(tag.name)} to apps/docs/appendix.ts`);
|
console.warn(
|
||||||
|
`tag ${tag.slug}: no Appendix term; add ${JSON.stringify(tag.name)} to apps/docs/appendix.ts`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const topicIcons = new Set(Object.values(TOPIC_ICONS));
|
const topicIcons = new Set(Object.values(TOPIC_ICONS));
|
||||||
const termIcons = new Map<string, string>(); // icon -> the term that took it first
|
const termIcons = new Map<string, string>(); // icon -> the term that took it first
|
||||||
for (const [slug, term] of Object.entries(APPENDIX)) {
|
for (const [slug, term] of Object.entries(APPENDIX)) {
|
||||||
if (TOPIC_LANDINGS[slug]) console.warn(`appendix ${slug}: a campaign topic teaches it now; delete its term`);
|
if (TOPIC_LANDINGS[slug])
|
||||||
if (topicIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is a topic's (TOPIC_ICONS); pick another`);
|
console.warn(
|
||||||
else if (termIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is ${termIcons.get(term.icon)}'s too; pick another`);
|
`appendix ${slug}: a campaign topic teaches it now; delete its term`,
|
||||||
|
);
|
||||||
|
if (topicIcons.has(term.icon))
|
||||||
|
console.warn(
|
||||||
|
`appendix ${slug}: icon ${JSON.stringify(term.icon)} is a topic's (TOPIC_ICONS); pick another`,
|
||||||
|
);
|
||||||
|
else if (termIcons.has(term.icon))
|
||||||
|
console.warn(
|
||||||
|
`appendix ${slug}: icon ${JSON.stringify(term.icon)} is ${termIcons.get(term.icon)}'s too; pick another`,
|
||||||
|
);
|
||||||
else termIcons.set(term.icon, slug);
|
else termIcons.set(term.icon, slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,14 +913,13 @@ for (const [dir, phase] of phases) {
|
|||||||
|
|
||||||
// Script-owned folder metas — one per live topic folder — rewritten only when
|
// Script-owned folder metas — one per live topic folder — rewritten only when
|
||||||
// they differ.
|
// they differ.
|
||||||
const metas: [label: string, dir: string, meta: string][] = [...groups].map(([dir, topic]) => [
|
const metas: [label: string, dir: string, meta: string][] = [...groups].map(
|
||||||
`topic ${topic.order ?? "-"}`,
|
([dir, topic]) => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)],
|
||||||
dir,
|
);
|
||||||
groupMeta(topic),
|
|
||||||
]);
|
|
||||||
for (const [label, dir, meta] of metas) {
|
for (const [label, dir, meta] of metas) {
|
||||||
const path = join(DOCS, dir, "meta.ts");
|
const path = join(DOCS, dir, "meta.ts");
|
||||||
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;
|
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta)
|
||||||
|
continue;
|
||||||
await Bun.write(path, meta);
|
await Bun.write(path, meta);
|
||||||
report.push([label, `${dir}/meta.ts`, "written"]);
|
report.push([label, `${dir}/meta.ts`, "written"]);
|
||||||
}
|
}
|
||||||
@@ -835,4 +947,5 @@ const w0 = Math.max(...report.map((r) => r[0].length), 9);
|
|||||||
const w1 = Math.max(...report.map((r) => r[1].length), 9);
|
const w1 = Math.max(...report.map((r) => r[1].length), 9);
|
||||||
console.log(`${"work file".padEnd(w0)} ${"docs page".padEnd(w1)} status`);
|
console.log(`${"work file".padEnd(w0)} ${"docs page".padEnd(w1)} status`);
|
||||||
console.log(`${"─".repeat(w0)} ${"─".repeat(w1)} ${"─".repeat(20)}`);
|
console.log(`${"─".repeat(w0)} ${"─".repeat(w1)} ${"─".repeat(20)}`);
|
||||||
for (const [a, b, c] of report) console.log(`${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`);
|
for (const [a, b, c] of report)
|
||||||
|
console.log(`${a.padEnd(w0)} ${b.padEnd(w1)} ${c}`);
|
||||||
Reference in new issue
Block a user