mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
docs(docs): add BacklinksList component and shared tagTarget logic for badge routing, update docs pages
This commit is contained in:
1 parent
eea8d242e0
commit
40e965aa06
5 files changed
+161
-23
No files matched your search
@@ -13,5 +13,7 @@ export default defineComponents({
|
||||
layout: {
|
||||
// Problem pages' topic + explanation badges, from `topics:` / `video:` frontmatter.
|
||||
PageHeader: "./components/ProblemBadges.astro",
|
||||
// Topic landing + guide pages' "what links here": every problem pointing at the page.
|
||||
PageFooter: "./components/BacklinksList.astro",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
/**
|
||||
* The `PageFooter` slot: an Obsidian-style backlinks list under a topic landing
|
||||
* or guide page — every problem page that points at it, as `<num> <name>`
|
||||
* rows in LeetCode-number order. Pages nothing points at (problem pages, the
|
||||
* overview, the changelog) render nothing.
|
||||
*
|
||||
* A problem points at a page when either
|
||||
* - one of its topic badges links there — the same `tagTarget()` answer
|
||||
* `ProblemBadges.astro` renders, so this list and the badges cannot drift
|
||||
* (a DFS problem is a backlink of the one DFS landing its badge picks); or
|
||||
* - it lives under the page's route (`/hybrid-structures/lru-cache` under
|
||||
* `/hybrid-structures`), so a topic no LeetCode tag names still lists its
|
||||
* own problems.
|
||||
*
|
||||
* Problem pages are recognised by their `<num>. <name>` title, the shape
|
||||
* `apps/cli/sync.ts` writes.
|
||||
*/
|
||||
import { getEntry } from "astro:content";
|
||||
import data from "blume:data";
|
||||
import { withBase } from "blume/components/islands/base-path.ts";
|
||||
import { tagTarget, type TopicTag } from "../topic-landings.ts";
|
||||
|
||||
interface Props {
|
||||
route: string;
|
||||
}
|
||||
|
||||
interface Route {
|
||||
path: string;
|
||||
title: string;
|
||||
collection: string;
|
||||
entryId: string;
|
||||
}
|
||||
|
||||
const { route } = Astro.props;
|
||||
const routes: Route[] = data.routes;
|
||||
const served = new Set(routes.map((r) => r.path));
|
||||
|
||||
const problems = routes.flatMap((r) => {
|
||||
const m = r.title.match(/^(\d+)\.\s+(.+)$/);
|
||||
return m ? [{ route: r, num: Number(m[1]), name: m[2]! }] : [];
|
||||
});
|
||||
|
||||
// A problem page is never a backlink target: skip the per-problem lookups there.
|
||||
const backlinks = problems.some((p) => p.route.path === route)
|
||||
? []
|
||||
: (
|
||||
await Promise.all(
|
||||
problems.map(async (p) => {
|
||||
if (p.route.path.startsWith(`${route}/`)) return p;
|
||||
const entry = await getEntry(p.route.collection, p.route.entryId);
|
||||
const tags: TopicTag[] = entry?.data.topics ?? [];
|
||||
const page = { route: p.route.path, tags };
|
||||
return tags.some((tag) => tagTarget(tag, page, served).href === route) ? p : undefined;
|
||||
}),
|
||||
)
|
||||
)
|
||||
.filter((p) => p !== undefined)
|
||||
.sort((a, b) => a.num - b.num);
|
||||
---
|
||||
|
||||
{
|
||||
backlinks.length > 0 && (
|
||||
<section aria-labelledby="backlinks" class="not-prose mx-auto mt-12 max-w-content border-border border-t pt-6">
|
||||
<h2 class="mb-2 flex items-baseline gap-2 font-medium text-muted-foreground text-xs uppercase tracking-wide" id="backlinks">
|
||||
Backlinks
|
||||
<span class="tabular-nums">{backlinks.length}</span>
|
||||
</h2>
|
||||
<ul class="text-sm">
|
||||
{backlinks.map((p) => (
|
||||
<li>
|
||||
<a
|
||||
class="-mx-2 flex items-baseline gap-3 rounded-md px-2 py-1 text-foreground no-underline transition-colors hover:bg-muted"
|
||||
href={withBase(p.route.path)}
|
||||
>
|
||||
<span class="w-10 shrink-0 text-right font-mono text-muted-foreground text-xs tabular-nums">{p.num}</span>
|
||||
<span class="truncate">{p.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -10,33 +10,23 @@
|
||||
* topic tags. A tag the campaign teaches (`topic-landings.ts`) links to that
|
||||
* topic's landing page and reads accent; any other tag links to its guide
|
||||
* page, `/guides/<slug>` (sync creates one per such tag), and reads neutral.
|
||||
* `tagTarget()` picks the route (and between two landings, e.g. DFS); the
|
||||
* `PageFooter` backlinks list reads the same answer.
|
||||
* - `video:` (problem pages, written by `apps/cli/videos.ts`) is the NeetCode
|
||||
* walkthrough's YouTube id, the red "Explanation" badge at the end of the row.
|
||||
* - `list:` (guide pages, written by sync on create) is the tag's LeetCode
|
||||
* problem list, an orange "LeetCode list" badge.
|
||||
*
|
||||
* A tag naming two campaign topics (`Depth-First Search` is both Tree DFS and
|
||||
* Graph DFS) 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 for a problem
|
||||
* LeetCode also tags `Tree` and the graph topic for the rest. A route the site
|
||||
* does not serve (a landing or guide page removed) falls back to LeetCode's
|
||||
* problem list for the tag, so a badge never links to a 404.
|
||||
*/
|
||||
import { getEntry } from "astro:content";
|
||||
import data from "blume:data";
|
||||
import Badge from "blume/components/content/Badge.astro";
|
||||
import { withBase } from "blume/components/islands/base-path.ts";
|
||||
import { TOPIC_LANDINGS } from "../topic-landings.ts";
|
||||
import { tagTarget, type TopicTag } from "../topic-landings.ts";
|
||||
|
||||
interface Props {
|
||||
route: string;
|
||||
}
|
||||
|
||||
interface TopicTag {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const { route } = Astro.props;
|
||||
const at = data.routes.find((r: { path: string }) => r.path === route);
|
||||
const entry = at ? await getEntry(at.collection, at.entryId) : undefined;
|
||||
@@ -44,17 +34,12 @@ const tags: TopicTag[] = entry?.data.topics ?? [];
|
||||
const video: string | undefined = entry?.data.video;
|
||||
const list: string | undefined = entry?.data.list;
|
||||
|
||||
const served = new Set(data.routes.map((r: { path: string }) => r.path));
|
||||
const own = `/${route.split("/")[1]}`;
|
||||
const prefer = tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-";
|
||||
const served = new Set<string>(data.routes.map((r: { path: string }) => r.path));
|
||||
|
||||
const badges = tags.map((tag) => {
|
||||
const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path));
|
||||
const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]);
|
||||
if (landing) return { name: tag.name, href: withBase(landing), accent: true, external: false };
|
||||
const guide = `/guides/${tag.slug}`;
|
||||
if (served.has(guide)) return { name: tag.name, href: withBase(guide), accent: false, external: false };
|
||||
return { name: tag.name, href: `https://leetcode.com/problem-list/${tag.slug}/`, accent: false, external: true };
|
||||
const target = tagTarget(tag, { route, tags }, served);
|
||||
const external = target.kind === "external";
|
||||
return { name: tag.name, href: external ? target.href : withBase(target.href), accent: target.kind === "landing", external };
|
||||
});
|
||||
|
||||
const linkClass = "no-underline transition-opacity hover:opacity-80";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: '134. Gas Station'
|
||||
description: There are n gas stations along a circular route, where the amount of gas at the i^th station is gas[i]
|
||||
icon: dot
|
||||
topics:
|
||||
- { name: "Array", slug: "array" }
|
||||
- { name: "Greedy", slug: "greedy" }
|
||||
sidebar:
|
||||
label: 'Gas Station'
|
||||
badge: 'Medium'
|
||||
---
|
||||
|
||||
### Example 1:
|
||||
- Input: `gas = [1,2,3,4,5], cost = [3,4,5,1,2]`
|
||||
- Output: `3`
|
||||
- Explanation: Start at station `3` (index `3`) and fill up with `4` unit of `gas`. Your tank = `0 + 4 = 4` Travel to station `4`. Your tank = `4 - 1 + 5 = 8` Travel to station `0`. Your tank = `8 - 2 + 1 = 7` Travel to station `1`. Your tank = `7 - 3 + 2 = 6` Travel to station `2`. Your tank = `6 - 4 + 3 = 5` Travel to station `3`. The `cost` is `5`. Your `gas` is just enough to travel back to station `3`. Therefore, return `3` as the starting index.
|
||||
|
||||
### Example 2:
|
||||
- Input: `gas = [2,3,4], cost = [3,4,3]`
|
||||
- Output: `-1`
|
||||
- Explanation: You can't start at station `0` or `1`, as there is not enough `gas` to travel to the next station. Let's start at station `2` and fill up with `4` unit of `gas`. Your tank = `0 + 4 = 4` Travel to station `0`. Your tank = `4 - 3 + 2 = 3` Travel to station `1`. Your tank = `3 - 3 + 3 = 3` You cannot travel back to station `2`, as it requires `4` unit of `gas` but you only have `3`. Therefore, you can't travel around the circuit once no matter where you start.
|
||||
|
||||
### Constraints:
|
||||
|
||||
- `n == gas.length == cost.length`
|
||||
- `1 <= n <= 10^5`
|
||||
- `0 <= gas[i], cost[i] <= 10^4`
|
||||
- The input is generated such that the answer is unique.
|
||||
|
||||
## Solution
|
||||
|
||||
```py
|
||||
class Solution:
|
||||
def canCompleteCircuit(self, gas: list[int], cost: list[int]) -> int:
|
||||
```
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* LeetCode topic-tag slug → the campaign landing routes that teach it, in
|
||||
* default preference order. The one answer to "does the campaign cover this
|
||||
* tag", read by two sides that must agree:
|
||||
* 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 `/guides/<slug>`;
|
||||
* - `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/guides/<slug>.mdx` for
|
||||
* every tag a problem page carries that is NOT a key here.
|
||||
*
|
||||
@@ -33,3 +35,33 @@ export const TOPIC_LANDINGS: Record<string, readonly string[]> = {
|
||||
"dynamic-programming": ["/1d-dynamic-programming", "/multi-d-grid-dp"],
|
||||
greedy: ["/greedy"],
|
||||
};
|
||||
|
||||
/** One of LeetCode's topic tags, as a problem page's `topics:` frontmatter carries it. */
|
||||
export interface TopicTag {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* for a problem LeetCode also tags `Tree` and the graph topic for the rest.
|
||||
*/
|
||||
export function tagTarget(
|
||||
tag: TopicTag,
|
||||
page: { route: string; tags: readonly TopicTag[] },
|
||||
served: ReadonlySet<string>,
|
||||
): { href: string; kind: "landing" | "guide" | "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 = `/guides/${tag.slug}`;
|
||||
if (served.has(guide)) return { href: guide, kind: "guide" };
|
||||
return { href: `https://leetcode.com/problem-list/${tag.slug}/`, kind: "external" };
|
||||
}
|
||||
Reference in new issue
Block a user