diff --git a/apps/docs/appendix.ts b/apps/docs/appendix.ts new file mode 100644 index 0000000..55e4fb5 --- /dev/null +++ b/apps/docs/appendix.ts @@ -0,0 +1,232 @@ +/** + * The Appendix glossary: one term per LeetCode topic tag that problem pages + * carry but no campaign topic teaches (not a key of `topic-landings.ts`). + * `pages/appendix.astro` renders each as a card — icon, category, definition, + * and the solved problems carrying the tag — anchored at `/appendix#`, + * which is where `tagTarget()` sends that tag's badge on a problem page. + * + * Hand-written: `apps/cli/sync.ts` only checks it, warning about a carried + * tag with no entry (its card renders bare), an entry whose tag a campaign + * topic now teaches (dead weight, delete it), and an icon that is a topic's + * (`TOPIC_ICONS`) or another term's — the icon is also the one on every + * badge linking here, so it must not read as anything else. Plain data, no + * Astro imports: sync imports this file under Bun. + */ + +export type AppendixCategory = "Data Structures" | "Algorithms" | "Techniques"; + +export interface AppendixTerm { + /** LeetCode's tag name. */ + name: string; + /** Lucide icon, unique across terms and topics. */ + icon: string; + category: AppendixCategory; + /** One sentence: what the term means, the way a solution uses it. */ + definition: string; +} + +export const APPENDIX: Record = { + // ── data structures ────────────────────────────────────────────── + array: { + name: "Array", + icon: "list-ordered", + category: "Data Structures", + definition: "A contiguous, index-addressed sequence: O(1) access by position, O(n) to insert or delete in the middle.", + }, + "binary-tree": { + name: "Binary Tree", + icon: "git-branch", + category: "Data Structures", + definition: "A rooted tree in which every node has at most two children, left and right.", + }, + "directed-acyclic-graph": { + name: "Directed Acyclic Graph", + icon: "route", + category: "Data Structures", + definition: "A directed graph with no cycles, which is exactly what lets its nodes be put in a topological order.", + }, + "doubly-linked-list": { + name: "Doubly-Linked List", + icon: "link-2", + category: "Data Structures", + definition: "A linked list whose nodes point to both neighbours, so a node you already hold is unlinked in O(1).", + }, + graph: { + name: "Graph Theory", + icon: "share-2", + category: "Data Structures", + definition: "Nodes joined by edges, directed or not, weighted or not, stored as an adjacency list or matrix.", + }, + "k-d-tree": { + name: "K-D Tree", + icon: "axis-3d", + category: "Data Structures", + definition: "A binary tree that splits k-dimensional points on alternating axes, for nearest-neighbour and range queries.", + }, + queue: { + name: "Queue", + icon: "list-end", + category: "Data Structures", + definition: "First in, first out: push at the back, pop from the front, both O(1).", + }, + string: { + name: "String", + icon: "quote", + category: "Data Structures", + definition: "A sequence of characters; solutions lean on its small alphabet, its substrings, and its immutability.", + }, + tree: { + name: "Tree", + icon: "tree-deciduous", + category: "Data Structures", + definition: "A connected acyclic graph, usually rooted, in which every node but the root has exactly one parent.", + }, + + // ── algorithms ─────────────────────────────────────────────────── + "bidirectional-search": { + name: "Bidirectional Search", + icon: "move-horizontal", + category: "Algorithms", + definition: "Search from the start and the goal at once and stop where the frontiers meet, about 2·b^(d/2) states instead of b^d.", + }, + "binary-lifting": { + name: "Binary Lifting", + icon: "chevrons-up", + category: "Algorithms", + definition: "Precompute every node's 2^k-th ancestor so any k-th ancestor, and so the LCA, takes O(log n) jumps.", + }, + "bit-manipulation": { + name: "Bit Manipulation", + icon: "binary", + category: "Algorithms", + definition: "Treat integers as bit vectors: masks, shifts, and XOR in place of arithmetic or extra memory.", + }, + "bracket-sequences": { + name: "Bracket Sequences", + icon: "parentheses", + category: "Algorithms", + definition: "Strings of opening and closing brackets; balanced means the running depth never dips below zero and ends at zero.", + }, + "bucket-sort": { + name: "Bucket Sort", + icon: "container", + category: "Algorithms", + definition: "Scatter values into buckets by range or frequency, then read the buckets in order: linear when the key range is bounded.", + }, + "complete-knapsack": { + name: "Complete Knapsack", + icon: "package-plus", + category: "Algorithms", + definition: "Knapsack in which every item may be taken any number of times, so the DP walks capacity upward and reuses the row.", + }, + "divide-and-conquer": { + name: "Divide and Conquer", + icon: "split", + category: "Algorithms", + definition: "Split the input into independent parts, solve each recursively, and combine their answers.", + }, + "floyds-cycle-finding-algorithm": { + name: "Floyd's Cycle Finding Algorithm", + icon: "repeat", + category: "Algorithms", + definition: "A slow and a fast pointer meet inside any cycle; restarting one from the head finds the cycle's entry, in O(1) space.", + }, + "knapsack-problem": { + name: "Knapsack Problem", + icon: "backpack", + category: "Algorithms", + definition: "Pick items with weights and values to maximise value under a capacity; the 0/1 version is an O(n·W) DP.", + }, + "longest-common-subsequence": { + name: "Longest Common Subsequence", + icon: "diff", + category: "Algorithms", + definition: "The longest sequence that appears in order, not necessarily contiguously, in two strings: the classic O(n·m) 2-D DP.", + }, + "lowest-common-ancestor": { + name: "Lowest Common Ancestor", + icon: "git-merge", + category: "Algorithms", + definition: "The deepest node of a rooted tree that is an ancestor of both given nodes.", + }, + memoization: { + name: "Memoization", + icon: "database", + category: "Algorithms", + definition: "Cache a pure function's result per argument so every subproblem is solved once: top-down dynamic programming.", + }, + "merge-sort": { + name: "Merge Sort", + icon: "blend", + category: "Algorithms", + definition: "Sort each half recursively, then merge the two sorted halves: O(n log n), stable, O(n) extra space.", + }, + quickselect: { + name: "Quickselect", + icon: "crosshair", + category: "Algorithms", + definition: "Partition around a pivot and recurse into one side only, finding the k-th smallest element in O(n) on average.", + }, + quicksort: { + name: "Quicksort", + icon: "shuffle", + category: "Algorithms", + definition: "Partition around a pivot, then sort each side in place: O(n log n) on average, O(n²) at worst.", + }, + recursion: { + name: "Recursion", + icon: "iteration-ccw", + category: "Algorithms", + definition: "Solve a problem by calling the same function on smaller inputs until a base case answers directly.", + }, + simulation: { + name: "Simulation", + icon: "monitor-play", + category: "Algorithms", + definition: "Carry out the process the statement describes, step by step; the work is modelling the state faithfully.", + }, + sorting: { + name: "Sorting", + icon: "arrow-down-wide-narrow", + category: "Algorithms", + definition: "Order the input first so a later step, such as two pointers, a greedy choice, or binary search, becomes possible.", + }, + "tournament-sort": { + name: "Tournament Sort", + icon: "trophy", + category: "Algorithms", + definition: "A winner tree of pairwise comparisons: the root is the extreme element, and replaying one path yields the next in O(log n).", + }, + + // ── techniques ─────────────────────────────────────────────────── + combinatorics: { + name: "Combinatorics", + icon: "dices", + category: "Techniques", + definition: "Count arrangements and selections (permutations, combinations, Catalan numbers), usually modulo a prime.", + }, + counting: { + name: "Counting", + icon: "tally-5", + category: "Techniques", + definition: "Tally occurrences in a hash map or fixed array, then answer from the counts instead of the raw input.", + }, + design: { + name: "Design", + icon: "pencil-ruler", + category: "Techniques", + definition: "Implement a class to an API spec, choosing the internals so every operation meets its time bound.", + }, + geometry: { + name: "Geometry", + icon: "shapes", + category: "Techniques", + definition: "Points, lines, and areas on a plane or grid: distances, slopes, overlaps, and orientation by cross product.", + }, + math: { + name: "Math", + icon: "calculator", + category: "Techniques", + definition: "An arithmetic or number-theory insight (modular arithmetic, GCD, digit manipulation, a closed form) in place of brute force.", + }, +}; diff --git a/apps/docs/components.ts b/apps/docs/components.ts index 8ee4e7a..ceee537 100644 --- a/apps/docs/components.ts +++ b/apps/docs/components.ts @@ -14,7 +14,7 @@ export default defineComponents({ wrapper: "./components/ProblemBadges.astro", }, layout: { - // Topic landing + guide pages' "what links here": every problem pointing at the page. + // Topic landing pages' "what links here": every problem pointing at the page. PageFooter: "./components/BacklinksList.astro", }, }); diff --git a/apps/docs/pages/appendix.astro b/apps/docs/pages/appendix.astro new file mode 100644 index 0000000..1727380 --- /dev/null +++ b/apps/docs/pages/appendix.astro @@ -0,0 +1,293 @@ +--- +/** + * `/appendix`: the glossary of LeetCode topic tags the campaign meets without + * teaching them as a topic (`appendix.ts`), one card per term, filed A–Z under + * a letter index. A card is the target of that tag's badge on every problem + * page (`/appendix#`, from `tagTarget()`), so it carries what the badge + * shows (the term's icon in the tag's colour from `lib/tag-colors.ts`, over + * the same tag universe `ProblemBadges.astro` colours) and lists the problem + * pages whose badge links here: the backlinks a per-term page used to have. + * The filter is a custom element over static markup, the way the Carousel + * is built; with no JS the page is the full glossary. + */ +import { getEntry } from "astro:content"; +import data from "blume:data"; +import Icon from "blume/components/Icon.astro"; +import { withBase } from "blume/components/islands/base-path.ts"; +import PageLayout from "blume/components/layout/PageLayout.astro"; +import { APPENDIX } from "../appendix.ts"; +import { tagColors } from "../lib/tag-colors.ts"; +import { TOPIC_LANDINGS, tagTarget, type TopicTag } from "../topic-landings.ts"; + +const { config } = data; +const served = new Set(data.routes.map((r: { path: string }) => r.path)); + +// Problem pages (`. ` titles, the shape sync writes) with their tags. +const problems = await Promise.all( + data.routes + .filter((r: { title: string }) => /^\d+\.\s/u.test(r.title)) + .map(async (r: { path: string; title: string; collection: string; entryId: string }) => { + const entry = await getEntry(r.collection, r.entryId); + const [, num, name] = r.title.match(/^(\d+)\.\s+(.+)$/u)!; + return { + route: r.path, + num: Number(num), + name: name!, + difficulty: entry?.data.sidebar?.badge as string | undefined, + tags: (entry?.data.topics ?? []) as TopicTag[], + }; + }), +); + +const colors = tagColors( + [...Object.keys(TOPIC_LANDINGS), ...Object.keys(APPENDIX)], + problems.map((p) => p.tags.map((t) => t.slug)), +); + +const terms = Object.entries(APPENDIX) + .map(([slug, term]) => ({ + slug, + ...term, + color: colors.get(slug), + problems: problems + .filter((p) => p.tags.some((tag) => tagTarget(tag, p, served).href === `/appendix#${slug}`)) + .sort((a, b) => a.num - b.num), + })) + .sort((a, b) => a.name.localeCompare(b.name)); + +const sections = new Map(); +for (const term of terms) { + const letter = term.name[0]!.toUpperCase(); + sections.set(letter, [...(sections.get(letter) ?? []), term]); +} +const LETTERS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; +// A card's Solved list shows this many rows at a time; a longer one (Array has +// dozens) pages through them with numbered buttons under the list. +const PAGE_SIZE = 5; + +// The /progress cards' difficulty palette (--chart-1..3 in theme.css), as on +// the changelog, so a Hard reads the same colour everywhere. +const DIFFICULTY: Record = { + Easy: { label: "Easy", class: "border-chart-1/40 bg-chart-1/10 text-chart-1" }, + Medium: { label: "Med.", class: "border-chart-2/40 bg-chart-2/10 text-chart-2" }, + Hard: { label: "Hard", class: "border-chart-3/40 bg-chart-3/10 text-chart-3" }, +}; +const description = + "The LeetCode topics the campaign meets without teaching them as a topic: what each one means, and the solved problems that use it."; +--- + + +
+

Appendix

+

{description}

+ + +
+ + +
+ + { + [...sections].map(([letter, group]) => ( +
+

{letter}

+
+ {group.map((term) => ( +
+
+ + + +
+

+ + {term.name} + +

+

+ {term.category} +

+
+ + LeetCode ↗ + +
+

{term.definition}

+ {term.problems.length > 0 && ( +
+ + + Solved + {term.problems.length} + + + + + + {term.problems.length > PAGE_SIZE && ( + + )} + +
+ )} +
+ ))} +
+
+ )) + } + +
+
+
+ + diff --git a/apps/docs/topic-landings.ts b/apps/docs/topic-landings.ts index aa71c6a..c6e69cc 100644 --- a/apps/docs/topic-landings.ts +++ b/apps/docs/topic-landings.ts @@ -4,16 +4,19 @@ * tag", read by the sides that must agree: * * - `components/ProblemBadges.astro` links a covered tag's badge to its - * landing page, and every other tag's badge to its Appendix guide, `/appendix/`; - * - `components/BacklinksList.astro` lists, under a landing or guide page, - * every problem whose badge links there — both via `tagTarget()`; - * - `apps/cli/sync.ts` creates a placeholder `content/appendix/.mdx` for - * every tag a problem page carries that is NOT a key here. + * landing page, and every other tag's badge to its Appendix card, + * `/appendix#` (`appendix.ts`); + * - `components/BacklinksList.astro` lists, under a landing page, every + * problem whose badge links there — both via `tagTarget()`; + * - `apps/cli/sync.ts` warns about a tag a problem page carries that is + * neither a key here nor an Appendix term. * - * Add a key when a campaign topic starts teaching a tag; its guide page is then - * dead weight, delete it by hand. A tag naming two topics (DFS/BFS: tree and + * Add a key when a campaign topic starts teaching a tag; its Appendix term is + * then dead weight, delete it. A tag naming two topics (DFS/BFS: tree and * graph; DP: 1-D and grid) lists both; the badge picks per page. */ +import { APPENDIX } from "./appendix.ts"; + export const TOPIC_LANDINGS: Record = { "hash-table": ["/hash-table"], "prefix-sum": ["/prefix-sum"], @@ -43,9 +46,9 @@ export interface TopicTag { } /** - * Where a problem page's badge for `tag` points: a root-relative landing or - * guide route (the caller adds the base), or LeetCode's problem list when the - * site serves neither, so a badge never links to a 404. + * Where a problem page's badge for `tag` points: a root-relative landing + * route or Appendix anchor (the caller adds the base), or LeetCode's problem + * list when the site has neither, so a badge never links to a 404. * * A tag naming two landings links to the page's own topic — its route's first * segment — when that is one of them; otherwise DFS/BFS prefer the tree topic @@ -55,13 +58,12 @@ export function tagTarget( tag: TopicTag, page: { route: string; tags: readonly TopicTag[] }, served: ReadonlySet, -): { href: string; kind: "landing" | "guide" | "external" } { +): { href: string; kind: "landing" | "appendix" | "external" } { const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path)); const own = `/${page.route.split("/")[1]}`; const prefer = page.tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-"; const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]); if (landing) return { href: landing, kind: "landing" }; - const guide = `/appendix/${tag.slug}`; - if (served.has(guide)) return { href: guide, kind: "guide" }; + if (APPENDIX[tag.slug]) return { href: `/appendix#${tag.slug}`, kind: "appendix" }; return { href: `https://leetcode.com/problem-list/${tag.slug}/`, kind: "external" }; } diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index d13242a..a466480 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -8,7 +8,14 @@ "noEmit": true, "skipLibCheck": true, "allowImportingTsExtensions": true, - "paths": { "@/*": ["./*"] } + "paths": { + "@/components": ["./components"], + "@/components/*": ["./components/*"], + "@/lib": ["./lib"], + "@/lib/*": ["./lib/*"], + "@/hooks": ["./hooks"], + "@/hooks/*": ["./hooks/*"] + } }, "include": ["islands", "components/ui", "lib", "topic-landings.ts", "redirects.ts"] }