mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f9e4ecb3b | ||
|
|
5dc2265ee5 | ||
|
|
0ca130156d | ||
|
|
0d64df58f1 | ||
|
|
88d1767184 | ||
|
|
93381f2b69 | ||
|
|
58ab9db1d7 | ||
|
|
e189bb876e | ||
|
|
314f2bff10 | ||
|
|
082b67c44d | ||
|
|
ad7c0a2972 | ||
|
|
931c2c64be | ||
|
|
0a99557aac | ||
|
|
7875b43981 | ||
|
|
f34a9bb56d | ||
|
|
bfeadc0cdf | ||
|
|
ca05c32f6e | ||
|
|
ad129b99fd | ||
|
|
717feb7f70 | ||
|
|
bf0ac34a43 | ||
|
|
403a2d7f23 | ||
|
|
3445c47ad2 | ||
|
|
24a9c52260 | ||
|
|
9d535c6826 | ||
|
|
a2d32c5f06 | ||
|
|
c69582552b | ||
|
|
f0f479d6cf | ||
|
|
ac4dc654fc | ||
|
|
273148c08b | ||
|
|
7960dee431 | ||
|
|
0009075138 | ||
|
|
ca73d3235c | ||
|
|
97b62f95c9 | ||
|
|
1a440e55ce | ||
|
|
fd3b1952b1 | ||
|
|
82c26e2b6c | ||
|
|
f78ecb2bd4 | ||
|
|
b3d3c5fbc9 | ||
|
|
99a9248368 | ||
|
|
2a925f6a9a | ||
|
|
7d57faa958 |
No files matched your search
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Pre-commit: the docs pages under apps/docs/content (and the `/<num>` short
|
||||||
|
# links in apps/docs/redirects.ts) are generated from work/1 by `bun run sync`,
|
||||||
|
# and deploy.yml publishes whatever main holds. Regenerate them now and stage
|
||||||
|
# what sync changed, so a solution, its page and its redirect land in the same
|
||||||
|
# commit and a stale `## Solution` never reaches main.
|
||||||
|
#
|
||||||
|
# Sync reads the working tree, not the index, so the pages it writes only
|
||||||
|
# match this commit when work/1 has nothing unstaged: an untracked or
|
||||||
|
# half-staged solution would ship a page whose source is not in the commit.
|
||||||
|
# In that case refuse and say what to stage.
|
||||||
|
#
|
||||||
|
# Only paths that became dirty during sync are staged. Anything already dirty
|
||||||
|
# beforehand (hand-written `## Approach` prose in progress, a new index.mdx)
|
||||||
|
# stays where the author left it.
|
||||||
|
#
|
||||||
|
# Bypass for a deliberate partial commit: git commit --no-verify
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
unstaged=$(git status --porcelain -- work/1 | grep -v '^[MADR] ' || true)
|
||||||
|
if [ -n "$unstaged" ]; then
|
||||||
|
printf 'pre-commit: work/1 has unstaged changes; stage or stash them so the generated pages match this commit:\n%s\n' "$unstaged" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
before=$(mktemp)
|
||||||
|
trap 'rm -f "$before"' EXIT
|
||||||
|
git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort > "$before"
|
||||||
|
report=$(bun run --silent sync)
|
||||||
|
generated=$(git status --porcelain -- apps/docs/content apps/docs/redirects.ts | sort | comm -13 "$before" - | cut -c4-)
|
||||||
|
if [ -n "$generated" ]; then
|
||||||
|
printf '%s\n' "$report" | grep -v 'skipped (in sync)' >&2
|
||||||
|
printf '%s\n' "$generated" | xargs git add --
|
||||||
|
fi
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Pre-push: the docs pages under apps/docs/content are generated from work/1
|
|
||||||
# by `bun run sync`, and deploy.yml publishes whatever main holds. Regenerate
|
|
||||||
# them now and refuse the push if the result differs from the tree, so a
|
|
||||||
# solution never lands without its page (or with a stale `## Solution`).
|
|
||||||
#
|
|
||||||
# Bypass for a deliberate partial push: git push --no-verify
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
|
||||||
report=$(bun run --silent sync)
|
|
||||||
|
|
||||||
changed=$(git status --porcelain -- apps/docs/content)
|
|
||||||
if [ -n "$changed" ]; then
|
|
||||||
printf '%s\n' "$report" | grep -v 'skipped (in sync)' >&2
|
|
||||||
printf '\npre-push: apps/docs/content is out of sync with work/1 — commit the regenerated pages:\n%s\n' "$changed" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Report matches without closing anything
|
description: Report matches and would-be releases without closing or releasing anything
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
@@ -61,3 +61,39 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
bun apps/cli/sync-project-fields.ts
|
bun apps/cli/sync-project-fields.ts
|
||||||
|
|
||||||
|
# One timever release per finished topic (every core sub-issue closed),
|
||||||
|
# tagged at the instant the last core problem closed. It reads the same rule
|
||||||
|
# close-topics uses (apps/cli/topics.ts) straight off the sub-issues, so the
|
||||||
|
# push that finishes a topic releases it in this run — no waiting for Close
|
||||||
|
# Topics, which chains after this workflow. Reconciling: a run releases
|
||||||
|
# every finished topic still missing one, so a manual dispatch backfills and
|
||||||
|
# a problem closed by hand in the UI is picked up by the next run.
|
||||||
|
release:
|
||||||
|
needs: close
|
||||||
|
# Like close-topics: a partially failed close still closed some issues,
|
||||||
|
# and the script only reads state, so run unless the run was cancelled.
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write # push tags, create releases
|
||||||
|
issues: read
|
||||||
|
steps:
|
||||||
|
# Full history: each tag targets the last main commit before its topic
|
||||||
|
# finished, and backfilled tags reach far behind HEAD.
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
# The tagger of the annotated, backdated tags.
|
||||||
|
- run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# No install step: the script only uses Bun builtins + fetch.
|
||||||
|
- run: bun apps/cli/release-topics.ts
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run }}
|
||||||
@@ -30,7 +30,13 @@ jobs:
|
|||||||
- uses: oven-sh/setup-bun@v2 # skip if you use npm/pnpm
|
- uses: oven-sh/setup-bun@v2 # skip if you use npm/pnpm
|
||||||
|
|
||||||
- run: bun install --frozen-lockfile
|
- run: bun install --frozen-lockfile
|
||||||
|
# blume.config.ts pulls the /changelog pages from this repo's GitHub
|
||||||
|
# releases (githubReleases source) at build time. The token authenticates
|
||||||
|
# that fetch — required if the repo is private, and it lifts the shared
|
||||||
|
# runner's unauthenticated API rate limit. `contents: read` covers it.
|
||||||
- run: bun run build
|
- run: bun run build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/upload-pages-artifact@v3
|
- uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ flowchart LR
|
|||||||
work -->|close-solved.yml| PI[problem issues]
|
work -->|close-solved.yml| PI[problem issues]
|
||||||
work -->|close-solved.yml POST /admin/solved| D1[(D1 = SRS state)]
|
work -->|close-solved.yml POST /admin/solved| D1[(D1 = SRS state)]
|
||||||
PI -->|close-topics.yml| TI[topic issues]
|
PI -->|close-topics.yml| TI[topic issues]
|
||||||
|
PI -->|close-solved.yml release job| Rel[timever release per finished topic]
|
||||||
GH[GitHub issues = catalog] -->|nightly reconcile| D1
|
GH[GitHub issues = catalog] -->|nightly reconcile| D1
|
||||||
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
|
D1 -->|8 AM ET cron| Mail[digest email + one-tap links]
|
||||||
Mail -->|GET /log| D1
|
Mail -->|GET /log| D1
|
||||||
@@ -37,6 +38,7 @@ flowchart LR
|
|||||||
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
|
- **Determinism = idempotency.** Drill/gate sampling uses a seeded PRNG (`rng()` in `apps/api/src/srs.ts`, FNV-1a → mulberry32, seed = date / ISO week); the digest is keyed by ET date in `email_log`; one-tap links are unique on (problem, date, kind).
|
||||||
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
|
- **DST-proof crons:** each event has two UTC crons; code fires only on the computed ET hour (`etHour`), unit-checked in `apps/api/src/srs.test.ts`.
|
||||||
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
|
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
|
||||||
|
- **One "finished topic" rule, two consumers.** `apps/cli/topics.ts` (`coreSet()`: every `set:core` sub-issue closed, zero core = never) is read by `close-topics.ts` (closes the topic issue) and `release-topics.ts` (the `release` job of `close-solved.yml`). The release reads the sub-issues, not the topic issue, so the push that finishes a topic releases it in the same run instead of waiting for Close Topics to chain after it. Version = [timever](https://gist.github.com/twolfson/de1b004dd22536b8e668) (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the instant the last core problem closed; the tag is annotated and backdated to that instant, on the last first-parent main commit before it; the body is a changelog snapshot at that instant (a one-sentence summary first — Blume's changelog makes it the page's meta description — then closed on the finish day / earlier / open at release, with solution links at the tag). A release is found by its `<!-- release-topics: #N -->` body marker, never by tag name, so re-closing a core problem never mints a second one; an existing release keeps its tag and only has its body rewritten when the render changes. The docs `/changelog` is these releases (`githubReleases` in `apps/docs/blume.config.ts`, fetched at build with `GITHUB_TOKEN`), dated by `published_at` — which the API cannot backdate, so the Aug 24 – Sep 24 backfill reads Sep 25 there, and its true finish time lives in the version and the summary.
|
||||||
|
|
||||||
## Key Directories
|
## Key Directories
|
||||||
|
|
||||||
@@ -44,10 +46,10 @@ flowchart LR
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `work/<1\|3\|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/1/Easy/Array/1.two-sum.py`. The top directory is the spaced-repetition bucket: `1` = first solve, `3`/`7` = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
|
| `work/<1\|3\|7>/<Difficulty>/<Category>/<num>.<slug>.{js,py}` | Solutions, e.g. `work/1/Easy/Array/1.two-sum.py`. The top directory is the spaced-repetition bucket: `1` = first solve, `3`/`7` = the blind re-solves of that problem 3 and 7 days later. Machine-parsed header comment (title / `Difficulty:` / URL / `─` rule / statement) — preserve its exact shape |
|
||||||
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
| `apps/cli/` | Flat Bun TS workspace: automation entries (shebang + top-level await) and libraries (no shebang, side-effect-free on import). Root `bun run` scripts delegate here |
|
||||||
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `islands/`, `public/`). `content/<topic>/<num>-<slug>.mdx` plus one `meta.ts` per topic folder, all generated by sync: the folder is the README campaign topic slug (`tree-dfs/`, `heap-priority-queue/`) and a real route segment (`/tree-dfs/invert-binary-tree`), the `meta.ts` carries the exact label and topic number so the sidebar reads in curriculum order, and the page badge is the `work/` data-structure category. A hand-written `<topic>/index.mdx` (`hidden: true`) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (`sliding-window/index.mdx` → `/sliding-window`) and the hidden flag drops the duplicate row |
|
| `apps/docs/` | Blume site (`blume.config.ts`, `content/`, `components/` + `components.ts`, `islands/`, `public/`). `content/(0N-<phase>)/<topic>/<num>-<slug>.mdx` plus one `meta.ts` per phase and per topic folder, all generated by sync: the phase folder is a `(group)` (sidebar section, no route segment), the topic folder is the README campaign topic slug (`tree-dfs/`, `heap-priority-queue/`) and a real route segment (`/tree-dfs/invert-binary-tree`), the `meta.ts` files carry the exact labels, phase/topic numbers and icons so the sidebar reads in curriculum order with each topic a collapsible group, and the page badge is the `work/` data-structure category. A hand-written `(0N-<phase>)/<topic>/index.mdx` (`hidden: true`) is that topic's landing page: Blume ≥ 1.7.3 links the sidebar group row to it (`sliding-window/index.mdx` → `/sliding-window`) and the hidden flag drops the duplicate row. Two kinds of custom MDX component, both import-free on every page: `islands/<Name>.tsx` is hydrated React (auto-discovered; `ProgressDashboard`, `DependencySpine` — the README topic map as data, zoom/pan, nodes link to whatever route Blume's `/api/docs/navigation.json` reports for the topic folder, inert until a topic has pages), `components/<Name>.astro` registered in `components.ts` is static markup plus a custom element for behaviour, the way Blume's own `Frame` is built (`Carousel`/`CarouselImage`: `src` takes a URL, `/file.png`, or the page-relative `../../public/file.png`) |
|
||||||
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (the plan: `date → { topics | problems | gate | sweep }`, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) |
|
| `apps/api/` | Cloudflare Worker workspace: `src/` modules, `data/schedule.json` (the plan: `date → { topics | problems | gate | sweep }`, human-edited, bundled at deploy), `migrations/` (append-only; `0002` is the +3/+7 ladder rebuild) |
|
||||||
| `.github/workflows/` | deploy (docs → Pages, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`), close-topics, sync-d1 (issue edits → `/admin/reconcile`), spaced-repetition (daily 06:00 ET cron → today's review issue; `GITHUB_TOKEN` only, no secrets) |
|
| `.github/workflows/` | deploy (docs → Pages with `GITHUB_TOKEN` for the releases-backed changelog, Worker → Cloudflare, on every main push), close-solved (issues + `/admin/solved` + Project `Set`/`Difficulty`, needs `SRS_ADMIN_KEY` and `PROJECT_PAT`; then the `release` job: timever tag + release per finished topic, `contents: write`), close-topics, sync-d1 (issue edits → `/admin/reconcile`), spaced-repetition (daily 06:00 ET cron → today's review issue; `GITHUB_TOKEN` only, no secrets) |
|
||||||
| `.githooks/` | Tracked git hooks; `bun install` points `core.hooksPath` here (root `prepare` script, no husky). `pre-push` runs `bun run sync` and refuses the push while `apps/docs/content` differs from the tree, so a solution never reaches `main` (and `deploy.yml`) without its page. `git push --no-verify` for a deliberate partial push |
|
| `.githooks/` | Tracked git hooks; `bun install` points `core.hooksPath` here (root `prepare` script, no husky). `pre-commit` runs `bun run sync` and stages only what sync changed (pages, `apps/docs/redirects.ts`), so a solution, its page and its `/<num>` short link land in one commit; it refuses to run while `work/1` has anything unstaged (sync reads the tree, so the page would not match the commit), and leaves pre-existing dirt under `apps/docs/content` (in-progress `## Approach` prose) unstaged. `git commit --no-verify` for a deliberate partial commit |
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
@@ -67,13 +69,15 @@ bun run test # run ONE solution against LeetCode's judge (not a test
|
|||||||
# most recently tested first, stamped in the db on exit 0)
|
# most recently tested first, stamped in the db on exit 0)
|
||||||
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
|
bun run submit # submit ONE solution to LeetCode (newest scaffold first, nothing
|
||||||
# hidden; last_submitted stamped in the db on exit 0)
|
# hidden; last_submitted stamped in the db on exit 0)
|
||||||
bun run sync # work/1 → apps/docs/content/ pages (re-solves are never published)
|
bun run sync # work/1 → apps/docs/content/ pages + apps/docs/redirects.ts (/<num> short
|
||||||
|
# links; re-solves are never published)
|
||||||
bun run videos # docs pages with no <YouTube> embed → NeetCode video per page
|
bun run videos # docs pages with no <YouTube> embed → NeetCode video per page
|
||||||
# (channel search scraped, no API key; --write appends the
|
# (channel search scraped, no API key; --write appends the
|
||||||
# `## Explanation` embed; "no NeetCode match" pages stay untouched)
|
# `## Explanation` embed; "no NeetCode match" pages stay untouched)
|
||||||
bun run dev|build # Blume docs site (runs in apps/docs/)
|
bun run dev|build # Blume docs site (runs in apps/docs/)
|
||||||
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
bun run close-solved -- --dry-run # issue reconcilers (also DRY_RUN=1)
|
||||||
bun run close-topics -- --dry-run
|
bun run close-topics -- --dry-run
|
||||||
|
bun run release-topics -- --dry-run # prints every would-be topic release body
|
||||||
# close-solved also pushes the solved set to the Worker; point it at a local
|
# close-solved also pushes the solved set to the Worker; point it at a local
|
||||||
# one and nothing production is touched:
|
# one and nothing production is touched:
|
||||||
SRS_API=http://localhost:8787 SRS_ADMIN_KEY=$LINK_KEY bun run close-solved -- --dry-run
|
SRS_API=http://localhost:8787 SRS_ADMIN_KEY=$LINK_KEY bun run close-solved -- --dry-run
|
||||||
@@ -106,11 +110,11 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
|
|||||||
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`.
|
||||||
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/56) LeetCode Daily Digest`.
|
||||||
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
|
||||||
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the `<Badge>` line and the `## Solution` section are script-owned, the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category, unordered, after the topics) and the badge is the `work/1/<Difficulty>/<Category>/` category — topic folders and their `meta.ts` are rewritten every run, a page is `rename`d when its topic changes, and a folder every page has left is deleted unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one). Never use `(group)` folders here: they add no route segment, so a topic `index.mdx` inside one collides with the root `/`. The two hand-owned sections around the solution have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); `## Explanation` (the NeetCode `<YouTube>` embed, after `## Solution`) is appended by `bun run videos --write` (`apps/cli/videos.ts`).
|
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the `<Badge>` line and the `## Solution` section are script-owned, the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category at `order: 0`, ahead of topic 1) and the badge is the `work/1/<Difficulty>/<Category>/` category. Topics nest under their README phase (`## Phase N — Name` headings, shortened via `PHASE_ALIASES`): `content/(0N-<phase>)/<topic>/`, both `meta.ts` script-owned and rewritten every run (phase: `Phase N: Name`, `order: N`; topic: label, curriculum order, `TOPIC_ICONS` icon, `display: "group"`). The phase folder is a Blume `(group)` — no route segment, so `/tree-dfs` is unchanged and a landing page there is one level deep, never at `/` — and the topic folder is the real segment; never put a topic `index.mdx` directly inside a `(group)`. A page is `rename`d when its topic or phase changes; a folder every page has left is deleted (climbing into an emptied phase folder) unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one — so page-relative asset paths in a moved landing page are yours to fix). Slugs come from `slugify()`, which glues a leading number to the next token (`1d-dynamic-programming`) because Blume strips `<digits>-` prefixes from routes. The two hand-owned sections around the solution have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); `## Explanation` (the NeetCode `<YouTube>` embed, after `## Solution`) is appended by `bun run videos --write` (`apps/cli/videos.ts`).
|
||||||
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
|
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
|
||||||
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
|
- `apps/api/src/stats.ts` / `apps/cli/stats.ts` / `apps/cli/tui.ts` / `apps/cli/api.ts` — the read side. `buildStats()` is the ONE aggregation: phases, ladder, difficulty, per-topic temperature, gates, the 14-day due queue, and `heat` (every campaign day, zeros and future days included — the docs page windows the tail off it rather than asking a second question). Two readers consume it, the docs island and `bun run stats`, so its shape is both their contract: change all three together. `tui.ts` is hand-rolled ANSI (bars, sparklines, heat cells, alternate screen) on charts.ts's palette, and every chart differs by GLYPH as well as by colour so a piped or `NO_COLOR` dashboard still reads. The TUI recomputes nothing: the only local fact it shows is the `work/` solution count, which is inventory, not schedule.
|
||||||
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
|
- `README.md` live charts are Worker endpoints (`/chart/*.svg`, `/badge/gate.svg`, 5-min Camo cache); the docs `/progress` page fetches `/api/stats` client-side (`apps/docs/islands/ProgressDashboard.tsx`).
|
||||||
- `apps/docs/blume.config.ts` — site base `/leetcode`; `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`).
|
- `apps/docs/blume.config.ts` — site base `/leetcode`; imports `apps/docs/redirects.ts`, sync-generated (`/<num>` → `/<topic>/<slug>`, one row per page, exact root-relative paths — Blume adds the base and, for the static Pages build, emits a meta-refresh page per rule plus `blume-redirects.json`); `README.md` campaign table doubles as topic-map input to `sync.ts` (`readmeTopics()`, `TOPIC_ALIASES`, `PHASE_ALIASES`).
|
||||||
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
|
- Old `.github/srs/` JSON state, its `srs-*` Actions, and the one-shot `import-srs.ts` importer are RETIRED and deleted — do not resurrect; git history is the record.
|
||||||
|
|
||||||
## Runtime/Tooling Preferences
|
## Runtime/Tooling Preferences
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://x.com/basedprad"><img alt="follow" src="https://shieldcn.dev/x/follow/basedprad.svg" /></a>
|
<a href="https://x.com/basedprad"><img alt="follow" src="https://shieldcn.dev/x/follow/basedprad.svg" /></a>
|
||||||
<a href="https://leetcode.com/progress"><img alt="Custom badge" src="https://shieldcn.dev/badge/view-@prdlk.svg?logo=leetcode" /></a>
|
<a href="https://leetcode.com/progress"><img alt="Custom badge" src="https://shieldcn.dev/badge/view-@prdlk.svg?logo=leetcode" /></a>
|
||||||
<a href="https://lc.prad.nu"><img alt="badge" src="https://shieldcn.dev/badge/Read-Documentation.svg?logo=lu%3ABook" /></a>
|
<a href="https://lc.prd.lk"><img alt="badge" src="https://shieldcn.dev/badge/Read-Documentation.svg?logo=lu%3ABook" /></a>
|
||||||
<a href="https://lc.prad.nu/progress"><img alt="gate pass rate" src="https://srs-api.prdlk.workers.dev/badge/gate.svg" /></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
"REPO": "prdlk/leetcode",
|
"REPO": "prdlk/leetcode",
|
||||||
"FROM_EMAIL": "srs@prdlk.com",
|
"FROM_EMAIL": "srs@prdlk.com",
|
||||||
"TO_EMAIL": "prnk28@gmail.com",
|
"TO_EMAIL": "prnk28@gmail.com",
|
||||||
"DOCS_URL": "https://prdlk.github.io/leetcode",
|
"DOCS_URL": "https://lc.prd.lk",
|
||||||
"DOCS_ORIGIN": "https://prdlk.github.io,https://lc.prad.nu",
|
"DOCS_ORIGIN": "https://lc.prd.lk,https://prdlk.github.io",
|
||||||
"PUBLIC_URL": "https://srs-api.prdlk.workers.dev"
|
"PUBLIC_URL": "https://srs-api.prdlk.workers.dev"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* `set:optional` and `set:deferred` are extra credit. A topic is finished when
|
* `set:optional` and `set:deferred` are extra credit. A topic is finished when
|
||||||
* every one of its core sub-issues is closed; optional/deferred state is
|
* every one of its core sub-issues is closed; optional/deferred state is
|
||||||
* ignored, matching the day rule in each topic body ("Core problems first").
|
* ignored, matching the day rule in each topic body ("Core problems first").
|
||||||
|
* The rule lives in topics.ts, shared with release-topics.ts.
|
||||||
*
|
*
|
||||||
* Like close-solved.ts this reconciles state instead of reacting to an event
|
* Like close-solved.ts this reconciles state instead of reacting to an event
|
||||||
* payload: re-runs are no-ops, backfilling needs no special casing, and only
|
* payload: re-runs are no-ops, backfilling needs no special casing, and only
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
import { github } from "./github.ts";
|
import { github } from "./github.ts";
|
||||||
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||||
|
import { coreSet, readProblems } from "./topics.ts";
|
||||||
|
|
||||||
const DRY =
|
const DRY =
|
||||||
process.argv.includes("--dry-run") ||
|
process.argv.includes("--dry-run") ||
|
||||||
@@ -53,52 +55,13 @@ for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=topic&state=ope
|
|||||||
}
|
}
|
||||||
topics.sort((a, b) => a.number - b.number);
|
topics.sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
// ── core sub-issue state per topic ───────────────────────────────
|
|
||||||
|
|
||||||
interface Core {
|
|
||||||
/** Core sub-issues, closed and open alike, lowest number first. */
|
|
||||||
all: number[];
|
|
||||||
/** The core sub-issues still open — non-empty means the topic stays open. */
|
|
||||||
pending: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a topic's core sub-issue state. The sub_issues payload carries the full
|
|
||||||
* issue objects (state + labels), so no per-problem follow-up request is needed.
|
|
||||||
*/
|
|
||||||
async function readCore(topic: number): Promise<Core> {
|
|
||||||
const all: number[] = [];
|
|
||||||
const pending: number[] = [];
|
|
||||||
for await (const raw of gh.list(`/repos/${gh.repo}/issues/${topic}/sub_issues`)) {
|
|
||||||
if (!raw || typeof raw !== "object") continue;
|
|
||||||
if (!("number" in raw) || typeof raw.number !== "number") continue;
|
|
||||||
if (!("state" in raw) || typeof raw.state !== "string") continue;
|
|
||||||
if (!("labels" in raw) || !Array.isArray(raw.labels)) continue;
|
|
||||||
|
|
||||||
const core = raw.labels.some(
|
|
||||||
(label) =>
|
|
||||||
label &&
|
|
||||||
typeof label === "object" &&
|
|
||||||
"name" in label &&
|
|
||||||
label.name === "set:core",
|
|
||||||
);
|
|
||||||
if (!core) continue;
|
|
||||||
|
|
||||||
all.push(raw.number);
|
|
||||||
if (raw.state === "open") pending.push(raw.number);
|
|
||||||
}
|
|
||||||
all.sort((a, b) => a - b);
|
|
||||||
pending.sort((a, b) => a - b);
|
|
||||||
return { all, pending };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── reconcile ────────────────────────────────────────────────────
|
// ── reconcile ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const report: string[][] = [];
|
const report: string[][] = [];
|
||||||
let closed = 0;
|
let closed = 0;
|
||||||
|
|
||||||
for (const topic of topics) {
|
for (const topic of topics) {
|
||||||
const core = await readCore(topic.number);
|
const core = coreSet(await readProblems(gh, topic.number));
|
||||||
const progress = `${core.all.length - core.pending.length}/${core.all.length}`;
|
const progress = `${core.all.length - core.pending.length}/${core.all.length}`;
|
||||||
|
|
||||||
// A topic with no core sub-issues has nothing to complete: never close it,
|
// A topic with no core sub-issues has nothing to complete: never close it,
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"sync": "bun ./sync.ts",
|
"sync": "bun ./sync.ts",
|
||||||
"videos": "bun ./videos.ts",
|
"videos": "bun ./videos.ts",
|
||||||
"close-solved": "bun ./close-solved.ts",
|
"close-solved": "bun ./close-solved.ts",
|
||||||
"close-topics": "bun ./close-topics.ts"
|
"close-topics": "bun ./close-topics.ts",
|
||||||
|
"release-topics": "bun ./release-topics.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@clack/prompts": "^1.7.0",
|
"@clack/prompts": "^1.7.0",
|
||||||
|
|||||||
Executable
+333
@@ -0,0 +1,333 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Publish one GitHub release per finished topic, versioned by timever
|
||||||
|
* (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC — https://gist.github.com/twolfson/de1b004dd22536b8e668).
|
||||||
|
*
|
||||||
|
* "Finished" is topics.ts's rule — every core sub-issue closed — the same one
|
||||||
|
* close-topics.ts closes the topic issue on, and the version is the instant
|
||||||
|
* the LAST core problem closed. Keying off the sub-issues rather than the
|
||||||
|
* topic issue lets this run as the tail of Close Solved: the push that closes
|
||||||
|
* a topic's final core problem releases it in the same run, without waiting
|
||||||
|
* for close-topics to chain after it. The topic issue itself closes seconds
|
||||||
|
* later, on the same day.
|
||||||
|
*
|
||||||
|
* Each release is a changelog snapshot at that instant: the topic's problem
|
||||||
|
* sub-issues grouped as closed on the finish day (ET), closed earlier, and
|
||||||
|
* open at release, each with its set, difficulty, and the solution files as
|
||||||
|
* they stood in the tagged commit — the last main commit before the finish.
|
||||||
|
*
|
||||||
|
* The tag is annotated and backdated (tagger date = finish), so
|
||||||
|
* `git tag --sort=taggerdate` reads as the campaign's history even for tags
|
||||||
|
* minted by a backfill. The release's own published date cannot be backdated
|
||||||
|
* (the API has no field for it), and the docs changelog (githubReleases in
|
||||||
|
* apps/docs/blume.config.ts) dates entries by it — so a backfilled entry
|
||||||
|
* carries its real finish time in the version and the body's first sentence.
|
||||||
|
*
|
||||||
|
* Reconciles instead of reacting: every finished topic without a release gets
|
||||||
|
* one, so a first run backfills the whole campaign; an existing release keeps
|
||||||
|
* its tag and commit and only has its body rewritten when render() would say
|
||||||
|
* something else. Re-runs with nothing new are no-ops.
|
||||||
|
* A release is found by the marker comment in its body, never by tag name, so
|
||||||
|
* reopening and re-closing a core problem can never mint a second release.
|
||||||
|
* A tag that exists without its release (a run that died between the two) is
|
||||||
|
* reused, not re-created.
|
||||||
|
*
|
||||||
|
* bun apps/cli/release-topics.ts # tag, push, release, fix bodies
|
||||||
|
* bun apps/cli/release-topics.ts --dry-run # print the would-be releases
|
||||||
|
*
|
||||||
|
* Auth: GH_TOKEN / GITHUB_TOKEN, else `gh auth token` (needs contents: write).
|
||||||
|
* Git: a clone with full history, an `origin` it may push tags to, and a
|
||||||
|
* committer identity (the tagger).
|
||||||
|
*/
|
||||||
|
import { etDate } from "../api/src/srs.ts";
|
||||||
|
import { github } from "./github.ts";
|
||||||
|
import { markdownTable, printTable, writeStepSummary } from "./report.ts";
|
||||||
|
import { coreSet, labelValue, readProblems, type Problem } from "./topics.ts";
|
||||||
|
import { ROOT, parseWorkPath } from "./work.ts";
|
||||||
|
|
||||||
|
const DRY =
|
||||||
|
process.argv.includes("--dry-run") ||
|
||||||
|
process.env.DRY_RUN === "1" ||
|
||||||
|
process.env.DRY_RUN === "true";
|
||||||
|
|
||||||
|
/** The docs site (apps/docs/blume.config.ts `deployment.site`); sync publishes `/<num>` short links. */
|
||||||
|
const DOCS_SITE = "https://lc.prd.lk";
|
||||||
|
const TIMEVER = "https://gist.github.com/twolfson/de1b004dd22536b8e668";
|
||||||
|
|
||||||
|
const gh = await github();
|
||||||
|
|
||||||
|
function git(...args: string[]): string {
|
||||||
|
const run = Bun.spawnSync(["git", ...args], { cwd: ROOT });
|
||||||
|
if (run.exitCode !== 0) throw new Error(`git ${args.join(" ")}: ${run.stderr.toString().trim()}`);
|
||||||
|
return run.stdout.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── topics ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Topic {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
milestone: string | null;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Narrow one /issues element; the `topic` label filter cannot exclude PRs. */
|
||||||
|
function readTopic(value: unknown): Topic | undefined {
|
||||||
|
if (!value || typeof value !== "object") return;
|
||||||
|
if ("pull_request" in value) return;
|
||||||
|
if (!("number" in value) || typeof value.number !== "number") return;
|
||||||
|
if (!("title" in value) || typeof value.title !== "string") return;
|
||||||
|
const milestone =
|
||||||
|
"milestone" in value &&
|
||||||
|
value.milestone &&
|
||||||
|
typeof value.milestone === "object" &&
|
||||||
|
"title" in value.milestone &&
|
||||||
|
typeof value.milestone.title === "string"
|
||||||
|
? value.milestone.title
|
||||||
|
: null;
|
||||||
|
const body = "body" in value && typeof value.body === "string" ? value.body : "";
|
||||||
|
return { number: value.number, title: value.title, milestone, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Finished {
|
||||||
|
topic: Topic;
|
||||||
|
problems: Problem[];
|
||||||
|
core: number[];
|
||||||
|
finishedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed and open topics alike: the rule reads the sub-issues, so a topic
|
||||||
|
// finished moments ago (close-topics not yet run) releases now.
|
||||||
|
const finished: Finished[] = [];
|
||||||
|
for await (const raw of gh.list(`/repos/${gh.repo}/issues?labels=topic&state=all`)) {
|
||||||
|
const topic = readTopic(raw);
|
||||||
|
if (!topic) continue;
|
||||||
|
const problems = await readProblems(gh, topic.number);
|
||||||
|
const core = coreSet(problems);
|
||||||
|
if (core.finishedAt) finished.push({ topic, problems, core: core.all, finishedAt: core.finishedAt });
|
||||||
|
}
|
||||||
|
// Oldest first, so a backfill creates releases in campaign order.
|
||||||
|
finished.sort((a, b) => a.finishedAt.localeCompare(b.finishedAt) || a.topic.number - b.topic.number);
|
||||||
|
|
||||||
|
// ── existing releases ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Topic -> its release. The body is kept in sync with render() on every run. */
|
||||||
|
const released = new Map<number, { id: number; tag: string; body: string }>();
|
||||||
|
for await (const raw of gh.list(`/repos/${gh.repo}/releases`)) {
|
||||||
|
if (!raw || typeof raw !== "object") continue;
|
||||||
|
if (!("id" in raw) || typeof raw.id !== "number") continue;
|
||||||
|
if (!("tag_name" in raw) || typeof raw.tag_name !== "string") continue;
|
||||||
|
if (!("body" in raw) || typeof raw.body !== "string") continue;
|
||||||
|
const m = raw.body.match(/<!-- release-topics: #(\d+) -->/);
|
||||||
|
if (m) released.set(Number(m[1]), { id: raw.id, tag: raw.tag_name, body: raw.body });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── timever ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timever of an instant. The API reports whole seconds, so SUBTIME is the
|
||||||
|
* tiebreak: two topics finishing in the same second (one push closing both
|
||||||
|
* last core problems) get consecutive nanoseconds, in (instant, topic) order.
|
||||||
|
*/
|
||||||
|
const tags = new Map<number, string>(); // topic -> tag
|
||||||
|
{
|
||||||
|
let prev = "";
|
||||||
|
let ns = 0;
|
||||||
|
for (const f of finished) {
|
||||||
|
const iso = new Date(f.finishedAt).toISOString(); // 2026-09-24T18:36:38.000Z
|
||||||
|
const base = `${iso.slice(0, 10).replaceAll("-", "")}.${iso.slice(11, 19).replaceAll(":", "")}`;
|
||||||
|
ns = base === prev ? ns + 1 : Number(iso.slice(20, 23)) * 1_000_000;
|
||||||
|
prev = base;
|
||||||
|
tags.set(f.topic.number, `${base}.${String(ns).padStart(9, "0")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── release body ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ET_WHEN = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
const ET_DAY = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Topic body sections worth carrying into the changelog (skipped while still `TODO`). */
|
||||||
|
const NOTES = ["Trigger", "Invariant", "Trap"] as const;
|
||||||
|
|
||||||
|
function bodyField(body: string, field: string): string | undefined {
|
||||||
|
return body.match(new RegExp(`^\\*\\*${field}:\\*\\*\\s*(.+)$`, "m"))?.[1]?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodySection(body: string, heading: string): string | undefined {
|
||||||
|
const m = body.match(new RegExp(`^### ${heading}\\n([\\s\\S]*?)(?=^### |(?![\\s\\S]))`, "m"));
|
||||||
|
const text = m?.[1]?.trim();
|
||||||
|
return text && text !== "TODO" ? text : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(f: Finished, tag: string, sha: string, files: Map<number, string[]>): string {
|
||||||
|
const { topic, problems } = f;
|
||||||
|
const finishDay = etDate(new Date(f.finishedAt));
|
||||||
|
// Snapshot at the finish instant: a problem closed later was open then.
|
||||||
|
const closedBy = (p: Problem) => p.closedAt !== null && p.closedAt <= f.finishedAt;
|
||||||
|
const onDay = problems.filter((p) => closedBy(p) && etDate(new Date(p.closedAt!)) === finishDay);
|
||||||
|
const earlier = problems.filter((p) => closedBy(p) && etDate(new Date(p.closedAt!)) !== finishDay);
|
||||||
|
const open = problems.filter((p) => !closedBy(p));
|
||||||
|
|
||||||
|
const row = (p: Problem): string[] => {
|
||||||
|
const lc = Number(p.title.match(/^LC (\d+)/)?.[1]);
|
||||||
|
const code = closedBy(p)
|
||||||
|
? (files.get(lc) ?? []).map(
|
||||||
|
(path) => `[${path.slice(path.lastIndexOf(".") + 1)}](https://github.com/${gh.repo}/blob/${tag}/${encodeURI(path)})`,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
return [
|
||||||
|
`#${p.number}`,
|
||||||
|
p.title,
|
||||||
|
labelValue(p, "set:") ?? "—",
|
||||||
|
labelValue(p, "diff:") ?? "—",
|
||||||
|
closedBy(p) ? ET_DAY.format(new Date(p.closedAt!)) : "—",
|
||||||
|
code.length ? [...code, `[docs](${DOCS_SITE}/${lc})`].join(" · ") : "—",
|
||||||
|
];
|
||||||
|
};
|
||||||
|
const table = (list: Problem[]) =>
|
||||||
|
markdownTable([["Issue", "Problem", "Set", "Difficulty", "Closed (ET)", "Solution"], ...list.map(row)]);
|
||||||
|
|
||||||
|
// Blume's changelog derives each page's meta description from the opening
|
||||||
|
// prose, stripping markdown and line breaks without adding a space — so lead
|
||||||
|
// with one self-contained sentence and keep the fields on one `·` line.
|
||||||
|
const when = ET_WHEN.format(new Date(f.finishedAt));
|
||||||
|
const summary =
|
||||||
|
`Finished ${when} ET: all ${f.core.length} core problems closed, ` +
|
||||||
|
`${onDay.length} ${onDay.length === 1 ? "problem" : "problems"} closed that day.`;
|
||||||
|
const fields: string[] = [];
|
||||||
|
const scheduled = bodyField(topic.body, "Scheduled");
|
||||||
|
if (scheduled) fields.push(`**Scheduled:** ${scheduled}`);
|
||||||
|
if (topic.milestone) fields.push(`**Phase:** ${topic.milestone}`);
|
||||||
|
fields.push(`**Topic:** #${topic.number}`);
|
||||||
|
const buildsOn = bodyField(topic.body, "Builds on");
|
||||||
|
if (buildsOn) fields.push(`**Builds on:** ${buildsOn}`);
|
||||||
|
// The marker is how a later run finds this release; see the file header.
|
||||||
|
const sections = [`<!-- release-topics: #${topic.number} -->\n${summary}`, fields.join(" · ")];
|
||||||
|
sections.push(`### Closed ${ET_DAY.format(new Date(f.finishedAt))} — the day the topic finished\n\n${table(onDay)}`);
|
||||||
|
if (earlier.length) sections.push(`### Closed earlier\n\n${table(earlier)}`);
|
||||||
|
if (open.length) sections.push(`### Open at release\n\n${table(open)}`);
|
||||||
|
const notes = NOTES.flatMap((h) => {
|
||||||
|
const text = bodySection(topic.body, h);
|
||||||
|
return text ? [`**${h}.** ${text}`] : [];
|
||||||
|
});
|
||||||
|
if (notes.length) sections.push(`### Notes\n\n${notes.join("\n\n")}`);
|
||||||
|
sections.push(
|
||||||
|
`---\n\n\`${tag}\` is [timever](${TIMEVER}) for the UTC instant the core set completed; ` +
|
||||||
|
`it tags ${sha.slice(0, 7)}, the last main commit before it.`,
|
||||||
|
);
|
||||||
|
return `${sections.join("\n\n")}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── reconcile ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const latest = finished.at(-1)?.topic.number;
|
||||||
|
const report: string[][] = [];
|
||||||
|
const previews: string[] = [];
|
||||||
|
let created = 0;
|
||||||
|
let updated = 0;
|
||||||
|
|
||||||
|
for (const f of finished) {
|
||||||
|
const { topic } = f;
|
||||||
|
const finishedEt = etDate(new Date(f.finishedAt));
|
||||||
|
const existing = released.get(topic.number);
|
||||||
|
// An existing release keeps its tag and commit; only its body is reconciled.
|
||||||
|
const tag = existing?.tag ?? tags.get(topic.number)!;
|
||||||
|
const sha = existing
|
||||||
|
? git("rev-parse", `${tag}^{commit}`)
|
||||||
|
: git("rev-list", "-1", "--first-parent", `--before=${f.finishedAt}`, "HEAD");
|
||||||
|
if (!sha) {
|
||||||
|
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "ERROR: no commit before finish"]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// LC number -> its first-solve files in the tagged tree. Keyed by file name,
|
||||||
|
// like firstSolved(): tags before the bucket split see `work/<Difficulty>/…`,
|
||||||
|
// and the work/3 + work/7 re-solves are not the solve that closed the issue.
|
||||||
|
const files = new Map<number, string[]>();
|
||||||
|
for (const path of git("ls-tree", "-r", "--name-only", sha, "--", "work").split("\n")) {
|
||||||
|
const bucket = parseWorkPath(path.slice("work/".length))?.bucket;
|
||||||
|
const lc = path.match(/\/(\d+)\.[^/]+\.(?:js|py)$/)?.[1];
|
||||||
|
if (!lc || (bucket && bucket !== 1)) continue;
|
||||||
|
files.set(Number(lc), [...(files.get(Number(lc)) ?? []), path]);
|
||||||
|
}
|
||||||
|
const body = render(f, tag, sha, files);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.body === body) {
|
||||||
|
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "released"]);
|
||||||
|
} else if (DRY) {
|
||||||
|
previews.push(`## ${tag} — ${topic.title}\n\n${body}`);
|
||||||
|
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would update"]);
|
||||||
|
} else {
|
||||||
|
await gh.api(`/repos/${gh.repo}/releases/${existing.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ body }),
|
||||||
|
});
|
||||||
|
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "updated"]);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DRY) {
|
||||||
|
previews.push(`## ${tag} — ${topic.title}\n\n${body}`);
|
||||||
|
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "would release"]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remote = git("ls-remote", "--tags", "origin", `refs/tags/${tag}`);
|
||||||
|
if (!remote) {
|
||||||
|
const local = Bun.spawnSync(["git", "rev-parse", "-q", "--verify", `refs/tags/${tag}`], { cwd: ROOT });
|
||||||
|
if (local.exitCode !== 0) {
|
||||||
|
const tagged = Bun.spawnSync(
|
||||||
|
["git", "tag", "-a", tag, sha, "-m", `${topic.title} (#${topic.number})\n\nCore set complete: ${f.core.map((n) => `#${n}`).join(", ")}`],
|
||||||
|
{ cwd: ROOT, env: { ...process.env, GIT_COMMITTER_DATE: f.finishedAt } },
|
||||||
|
);
|
||||||
|
if (tagged.exitCode !== 0) throw new Error(`git tag ${tag}: ${tagged.stderr.toString().trim()}`);
|
||||||
|
}
|
||||||
|
git("push", "origin", `refs/tags/${tag}`);
|
||||||
|
}
|
||||||
|
await gh.api(`/repos/${gh.repo}/releases`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
tag_name: tag,
|
||||||
|
name: topic.title,
|
||||||
|
body,
|
||||||
|
make_latest: topic.number === latest ? "true" : "false",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
report.push([`#${topic.number}`, topic.title, finishedEt, tag, "released now"]);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── report ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (previews.length) console.log(`${previews.join("\n")}\n`);
|
||||||
|
const rows = [["topic", "title", "finished (ET)", "tag", "status"], ...report];
|
||||||
|
printTable(rows);
|
||||||
|
const would = (status: string) => report.filter((r) => r[4] === status).length;
|
||||||
|
console.log(
|
||||||
|
`\n${finished.length} finished topics · ` +
|
||||||
|
(DRY
|
||||||
|
? `would release ${would("would release")}, update ${would("would update")}`
|
||||||
|
: `released ${created}, updated ${updated}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeStepSummary(
|
||||||
|
`### release-topics${DRY ? " (dry run)" : ""}\n\n` +
|
||||||
|
`${finished.length} finished topics, ${released.size} already released.\n\n` +
|
||||||
|
(report.length ? `${markdownTable(rows)}\n` : "No finished topics.\n") +
|
||||||
|
(previews.length ? `\n${previews.join("\n")}` : ""),
|
||||||
|
);
|
||||||
+202
-35
@@ -5,8 +5,8 @@
|
|||||||
* - New problems get a full page (frontmatter, badge, warning, examples,
|
* - New problems get a full page (frontmatter, badge, warning, examples,
|
||||||
* constraints, solution) in the gold-standard format.
|
* constraints, solution) in the gold-standard format.
|
||||||
* - Already-ported pages only get their script-owned parts regenerated — the
|
* - Already-ported pages only get their script-owned parts regenerated — the
|
||||||
* `<Badge>` line and the `## Solution` section — so hand-curated prose is
|
* frontmatter `icon:` (`PAGE_ICON`), the `<Badge>` line and the
|
||||||
* never touched.
|
* `## Solution` section — so hand-curated prose is never touched.
|
||||||
* - When a problem is solved in both JavaScript and Python the solution
|
* - When a problem is solved in both JavaScript and Python the solution
|
||||||
* renders as a <CodeGroup> (Python first); single-language solutions
|
* renders as a <CodeGroup> (Python first); single-language solutions
|
||||||
* render as a plain fence.
|
* render as a plain fence.
|
||||||
@@ -17,15 +17,23 @@
|
|||||||
* file sits under in `work/1/<Difficulty>/<Category>/`. A problem outside the
|
* file sits under in `work/1/<Difficulty>/<Category>/`. A problem outside the
|
||||||
* plan (warm-ups) has no topic, so it groups under its category instead.
|
* plan (warm-ups) has no topic, so it groups under its category instead.
|
||||||
*
|
*
|
||||||
* Topic folders are real route segments (`/tree-dfs/invert-binary-tree`), not
|
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
||||||
* `(group)` folders, so a hand-written `index.mdx` in one is the topic's
|
* The phase folder is a Blume `(group)` — a sidebar section with its own
|
||||||
* landing page at `/tree-dfs` and Blume links the sidebar group row to it.
|
* script-owned `meta.ts`, adding no route segment — while the topic folder is
|
||||||
* Sync never writes or moves an `index.mdx`, and never removes a folder that
|
* a real segment (`/tree-dfs/invert-binary-tree`), so a hand-written
|
||||||
* still holds one.
|
* `index.mdx` in it is the topic's landing page at `/tree-dfs` and Blume links
|
||||||
|
* the sidebar group row to it. Sync never writes or moves an `index.mdx`, and
|
||||||
|
* never removes a folder that still holds one. An unplanned category group has
|
||||||
|
* no phase and sits at the content root.
|
||||||
*
|
*
|
||||||
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
|
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
|
||||||
* of problems whose page already exists (see work.ts), and a fresh scaffold
|
* of problems whose page already exists (see work.ts), and a fresh scaffold
|
||||||
* there would otherwise wipe the page's `## Solution`.
|
* there would otherwise wipe the page's `## Solution`.
|
||||||
|
*
|
||||||
|
* Every page also gets a short link: `/<num>` → `/<topic>/<slug>` (so
|
||||||
|
* `/leetcode/217` opens Contains Duplicate). The rules live in the generated
|
||||||
|
* `apps/docs/redirects.ts`, which `blume.config.ts` imports — Blume takes exact
|
||||||
|
* root-relative paths and prepends the deployment base itself.
|
||||||
*/
|
*/
|
||||||
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
||||||
import { basename, dirname, join } from "node:path";
|
import { basename, dirname, join } from "node:path";
|
||||||
@@ -33,6 +41,7 @@ import { basename, dirname, join } from "node:path";
|
|||||||
const ROOT = join(import.meta.dir, "..", "..");
|
const ROOT = join(import.meta.dir, "..", "..");
|
||||||
const WORK = join(ROOT, "work", "1");
|
const WORK = join(ROOT, "work", "1");
|
||||||
const DOCS = join(ROOT, "apps", "docs", "content");
|
const DOCS = join(ROOT, "apps", "docs", "content");
|
||||||
|
const REDIRECTS = join(ROOT, "apps", "docs", "redirects.ts");
|
||||||
|
|
||||||
type Lang = "js" | "py";
|
type Lang = "js" | "py";
|
||||||
|
|
||||||
@@ -171,24 +180,86 @@ function parseHeader(header: string): Header {
|
|||||||
|
|
||||||
// ── README topic map ─────────────────────────────────────────────
|
// ── README topic map ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** A campaign phase: `## Phase N — Name · Weeks …` in the README. */
|
||||||
|
interface Phase {
|
||||||
|
order: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** A campaign topic: `<strong>NN. Name</strong>` in the README table. */
|
/** A campaign topic: `<strong>NN. Name</strong>` in the README table. */
|
||||||
interface Topic {
|
interface Topic {
|
||||||
order?: number; // README topic number; absent for an unplanned problem's category group
|
order?: number; // README topic number; absent for an unplanned problem's category group
|
||||||
name: string;
|
name: string;
|
||||||
|
phase?: Phase; // absent for an unplanned category group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** README phase names → the short labels the overview page uses. */
|
||||||
|
const PHASE_ALIASES: Record<string, string> = {
|
||||||
|
"Nodal & Grid Structures": "Nodal & Grid",
|
||||||
|
"Hierarchical Structures": "Hierarchical",
|
||||||
|
"Relational Structures": "Relational",
|
||||||
|
"Decision-Space Exploration": "Decision Space",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"];
|
||||||
|
|
||||||
const TOPIC_ALIASES: Record<string, string> = {
|
const TOPIC_ALIASES: Record<string, string> = {
|
||||||
"Hash-Based Lookup": "Hash Table",
|
"Hash-Based Lookup": "Hash Table",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidebar icon per group, keyed by the (post-alias) topic name — the same key an
|
||||||
|
* unplanned problem's category group uses, so `Stack` the category and `Stack`
|
||||||
|
* the topic share one row. Lucide names; Blume resolves them at build time and
|
||||||
|
* reports a diagnostic for a typo, but an unmapped group is caught here first.
|
||||||
|
*/
|
||||||
|
const TOPIC_ICONS: Record<string, string> = {
|
||||||
|
// README topics, in curriculum order
|
||||||
|
"Hash Table": "hash",
|
||||||
|
"Prefix Sum": "sigma",
|
||||||
|
"Two Pointers": "arrow-left-right",
|
||||||
|
"Sliding Window": "app-window",
|
||||||
|
"Binary Search": "search",
|
||||||
|
Stack: "layers",
|
||||||
|
"Monotonic Stack": "trending-up",
|
||||||
|
"Linked List": "link",
|
||||||
|
"Hybrid Structures": "combine",
|
||||||
|
"Matrix Index Math": "grid-3x3",
|
||||||
|
"Tree DFS": "folder-tree",
|
||||||
|
"Tree BFS": "network",
|
||||||
|
"Binary Search Tree": "git-fork",
|
||||||
|
"Heap / Priority Queue": "triangle",
|
||||||
|
Trie: "spell-check",
|
||||||
|
"Graph DFS": "waypoints",
|
||||||
|
"Graph BFS": "radar",
|
||||||
|
"Topological Sort": "workflow",
|
||||||
|
"Union-Find": "merge",
|
||||||
|
Backtracking: "undo-2",
|
||||||
|
"1-D Dynamic Programming": "columns-3",
|
||||||
|
"Multi-D / Grid DP": "table",
|
||||||
|
Greedy: "coins",
|
||||||
|
Intervals: "calendar-range",
|
||||||
|
// work/1 categories that can surface as an unplanned group
|
||||||
|
Array: "brackets",
|
||||||
|
String: "type",
|
||||||
|
Tree: "network",
|
||||||
|
"Depth-First Search": "folder-tree",
|
||||||
|
};
|
||||||
|
|
||||||
/** LC number -> topic. A problem listed under two topics keeps the first. */
|
/** LC number -> topic. A problem listed under two topics keeps the first. */
|
||||||
async function readmeTopics(): Promise<Map<number, Topic>> {
|
async function readmeTopics(): Promise<Map<number, Topic>> {
|
||||||
const md = await Bun.file(join(ROOT, "README.md")).text();
|
const md = await Bun.file(join(ROOT, "README.md")).text();
|
||||||
const map = new Map<number, Topic>();
|
const map = new Map<number, Topic>();
|
||||||
|
let phase: Phase | undefined;
|
||||||
let topic: Topic | null = null;
|
let topic: Topic | null = null;
|
||||||
for (const line of md.split("\n")) {
|
for (const line of md.split("\n")) {
|
||||||
|
const p = line.match(/^## Phase ([IVX]+) — (.+?) · /);
|
||||||
|
if (p) {
|
||||||
|
const name = p[2]!.replaceAll("&", "&");
|
||||||
|
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() };
|
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);
|
||||||
@@ -196,10 +267,53 @@ async function readmeTopics(): Promise<Map<number, Topic>> {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Blume folder meta: exact topic label (the folder slug loses `/` and case) and curriculum order. */
|
/**
|
||||||
|
* Folder slug for a topic or phase name. Blume reads a leading `<digits>-` on a
|
||||||
|
* folder as a sidebar-order prefix and strips it from the route (`1-d-…` →
|
||||||
|
* `/d-…`), so a slug that would start that way keeps the digits glued to the
|
||||||
|
* next token (`1d-dynamic-programming`).
|
||||||
|
*/
|
||||||
|
function slugify(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "")
|
||||||
|
.replace(/^(\d+)-/, "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content folder for a topic, relative to `DOCS`: `(0N-<phase>)/<topic>` for a
|
||||||
|
* planned topic (the phase is a `(group)`, so the route is still `/<topic>`),
|
||||||
|
* bare `<topic>` for an unplanned category group.
|
||||||
|
*/
|
||||||
|
function topicDir(topic: Topic): string {
|
||||||
|
const slug = slugify(topic.name);
|
||||||
|
return topic.phase ? `${phaseDir(topic.phase)}/${slug}` : slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
function phaseDir(phase: Phase): string {
|
||||||
|
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blume `(group)` meta for a phase: the overview page's heading, in phase order. */
|
||||||
|
function phaseMeta(phase: Phase): string {
|
||||||
|
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(`Phase ${phase.order}: ${phase.name}`)}, order: ${phase.order} });\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blume folder meta for a topic: exact label (the folder slug loses `/` and
|
||||||
|
* case), curriculum order, the sidebar icon, and `display: "group"` so the
|
||||||
|
* topic collapses under its phase heading. An unplanned category group
|
||||||
|
* (warm-ups) has no README number and sorts at 0, ahead of phase 1, so the
|
||||||
|
* sidebar opens on the basics.
|
||||||
|
*/
|
||||||
function groupMeta(topic: Topic): string {
|
function groupMeta(topic: Topic): string {
|
||||||
const order = topic.order === undefined ? "" : `, order: ${topic.order}`;
|
const icon = TOPIC_ICONS[topic.name];
|
||||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(topic.name)}${order} });\n`;
|
if (!icon) 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)}`);
|
||||||
|
fields.push(`display: "group"`);
|
||||||
|
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── inline-code formatting heuristics ────────────────────────────
|
// ── inline-code formatting heuristics ────────────────────────────
|
||||||
@@ -303,6 +417,9 @@ function warningText(h: Header): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sidebar/page icon for every problem page (Lucide, resolved by Blume). */
|
||||||
|
const PAGE_ICON = "dot";
|
||||||
|
|
||||||
function badgeLine(sol: Solution): string {
|
function badgeLine(sol: Solution): string {
|
||||||
return `<Badge variant="accent">${sol.category}</Badge>`;
|
return `<Badge variant="accent">${sol.category}</Badge>`;
|
||||||
}
|
}
|
||||||
@@ -317,6 +434,7 @@ function renderPage(sol: Solution): string {
|
|||||||
"---",
|
"---",
|
||||||
`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}`,
|
||||||
"sidebar:",
|
"sidebar:",
|
||||||
` label: '${h.title.replaceAll("'", "''")}'`,
|
` label: '${h.title.replaceAll("'", "''")}'`,
|
||||||
` badge: '${h.difficulty}'`,
|
` badge: '${h.difficulty}'`,
|
||||||
@@ -380,6 +498,11 @@ for await (const rel of new Bun.Glob("**/*.{js,py}").scan(WORK)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const topics = await readmeTopics();
|
const topics = await readmeTopics();
|
||||||
|
// An unplanned problem whose work/ category is also a README topic name (a
|
||||||
|
// `Hash Table` warm-up) files under that topic, in its phase, not a second
|
||||||
|
// root-level `hash-table/`.
|
||||||
|
const topicByName = new Map<string, Topic>();
|
||||||
|
for (const topic of topics.values()) topicByName.set(topic.name, topic);
|
||||||
|
|
||||||
/** Insert a number-free `sidebar.label` into existing frontmatter when missing. */
|
/** Insert a number-free `sidebar.label` into existing frontmatter when missing. */
|
||||||
function ensureLabel(page: string, h: Header): string {
|
function ensureLabel(page: string, h: Header): string {
|
||||||
@@ -389,6 +512,17 @@ function ensureLabel(page: string, h: Header): string {
|
|||||||
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
|
return page.replace(/^sidebar:$/m, `sidebar:\n${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the frontmatter's top-level `icon:` to `PAGE_ICON`, inserting it before `sidebar:`. */
|
||||||
|
function ensureIcon(page: string): string {
|
||||||
|
const fm = page.match(/^---\n[\s\S]*?\n---/);
|
||||||
|
if (!fm) return page;
|
||||||
|
const line = `icon: ${PAGE_ICON}`;
|
||||||
|
const next = /^icon: .*$/m.test(fm[0])
|
||||||
|
? fm[0].replace(/^icon: .*$/m, line)
|
||||||
|
: fm[0].replace(/^sidebar:$/m, `${line}\nsidebar:`);
|
||||||
|
return next + page.slice(fm[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offset just past the script-owned `## Solution` section: the next top-level
|
* Offset just past the script-owned `## Solution` section: the next top-level
|
||||||
* heading that is not inside a code fence, or end of file. Sections a human
|
* heading that is not inside a code fence, or end of file. Sections a human
|
||||||
@@ -417,22 +551,24 @@ for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).s
|
|||||||
|
|
||||||
const report: [string, string, string][] = [];
|
const report: [string, string, string][] = [];
|
||||||
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
|
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
|
||||||
const groups = new Map<string, Topic>(); // folder -> topic, for meta.ts
|
const groups = new Map<string, Topic>(); // topic folder -> topic, for meta.ts
|
||||||
|
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
|
||||||
|
|
||||||
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("+");
|
||||||
const workRef = `${sol.num}.${sol.slug} (${langs})`;
|
const workRef = `${sol.num}.${sol.slug} (${langs})`;
|
||||||
// <topic>/ folder (a real route segment, so its index.mdx is the topic landing
|
// (0N-<phase>)/<topic>/ folder (the topic is a real route segment, so its
|
||||||
// page) + leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
|
// index.mdx is the landing page) + leetcode-number prefix: numeric sidebar
|
||||||
// An unplanned problem's category may share a folder with a numbered topic
|
// order, prefix stripped from the URL.
|
||||||
// (`Hash Table` warm-ups under topic 1); the numbered one owns the meta.
|
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
|
||||||
const topic = topics.get(sol.num) ?? { name: sol.category };
|
const dir = topicDir(topic);
|
||||||
const group = topic.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
groups.set(dir, topic);
|
||||||
if (groups.get(group)?.order === undefined) groups.set(group, topic);
|
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
|
||||||
const name = `${group}/${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, group), { recursive: true });
|
await mkdir(join(DOCS, dir), { recursive: true });
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const page = await Bun.file(join(DOCS, existing)).text();
|
const page = await Bun.file(join(DOCS, existing)).text();
|
||||||
@@ -443,7 +579,7 @@ for (const sol of ordered) {
|
|||||||
}
|
}
|
||||||
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 = page.slice(0, at) + solutionSection(sol) + (tail ? `\n${tail}` : "");
|
||||||
const next = ensureLabel(body, sol.header).replace(/^<Badge variant="accent">.*<\/Badge>$/m, badgeLine(sol));
|
const next = ensureIcon(ensureLabel(body, sol.header)).replace(/^<Badge variant="accent">.*<\/Badge>$/m, badgeLine(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));
|
||||||
@@ -459,25 +595,56 @@ for (const sol of ordered) {
|
|||||||
await Bun.write(join(DOCS, name), renderPage(sol));
|
await Bun.write(join(DOCS, name), renderPage(sol));
|
||||||
report.push([workRef, name, "created"]);
|
report.push([workRef, name, "created"]);
|
||||||
}
|
}
|
||||||
|
// 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}`)} },`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Script-owned folder meta: one per live group, rewritten only when it differs.
|
// Script-owned short links, one per page; imported by blume.config.ts.
|
||||||
for (const [group, topic] of groups) {
|
{
|
||||||
const path = join(DOCS, group, "meta.ts");
|
const module = [
|
||||||
const meta = groupMeta(topic);
|
"// Generated by `bun run sync` (apps/cli/sync.ts) — do not edit.",
|
||||||
|
"// `/leetcode/<num>` → that problem's page; exact paths, base added by Blume.",
|
||||||
|
"export default [",
|
||||||
|
...redirects,
|
||||||
|
"];",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
if (!(await Bun.file(REDIRECTS).exists()) || (await Bun.file(REDIRECTS).text()) !== module) {
|
||||||
|
await Bun.write(REDIRECTS, module);
|
||||||
|
report.push([`${redirects.length} redirects`, "apps/docs/redirects.ts", "written"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script-owned folder metas — one per live phase and topic folder — rewritten
|
||||||
|
// only when they differ.
|
||||||
|
const metas: [label: string, dir: string, meta: string][] = [
|
||||||
|
...[...phases].map(([dir, phase]): [string, string, string] => [`phase ${phase.order}`, dir, phaseMeta(phase)]),
|
||||||
|
...[...groups].map(([dir, topic]): [string, string, string] => [`topic ${topic.order ?? "-"}`, dir, groupMeta(topic)]),
|
||||||
|
];
|
||||||
|
for (const [label, dir, meta] of metas) {
|
||||||
|
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([`topic ${topic.order ?? "-"}`, `${group}/meta.ts`, "written"]);
|
report.push([label, `${dir}/meta.ts`, "written"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A group every page left behind is dead: drop its meta and the folder itself.
|
// A folder every page left behind is dead: drop its meta and the folder itself,
|
||||||
for (const dir of vacated) {
|
// then the phase folder above it if that emptied too. A folder that still holds
|
||||||
if (groups.has(dir)) continue;
|
// anything else (a hand-written index.mdx, another topic) stops the climb.
|
||||||
const left = await readdir(join(DOCS, dir));
|
for (const start of vacated) {
|
||||||
if (left.some((f) => f !== "meta.ts")) continue;
|
for (let dir = start; dir !== "."; dir = dirname(dir)) {
|
||||||
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
|
if (groups.has(dir) || phases.has(dir)) break;
|
||||||
await rmdir(join(DOCS, dir));
|
let left: string[];
|
||||||
report.push(["", dir, "removed (empty)"]);
|
try {
|
||||||
|
left = await readdir(join(DOCS, dir));
|
||||||
|
} catch {
|
||||||
|
break; // already removed via a sibling's climb
|
||||||
|
}
|
||||||
|
if (left.some((f) => f !== "meta.ts")) break;
|
||||||
|
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
|
||||||
|
await rmdir(join(DOCS, dir));
|
||||||
|
report.push(["", dir, "removed (empty)"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const w0 = Math.max(...report.map((r) => r[0].length), 9);
|
const w0 = Math.max(...report.map((r) => r[0].length), 9);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Topic issues and their problem sub-issues: the ONE reading of "is this topic
|
||||||
|
* finished", shared by close-topics.ts (closes the topic issue) and
|
||||||
|
* release-topics.ts (publishes the topic's timever release) so the two can
|
||||||
|
* never disagree about when a topic was done.
|
||||||
|
*
|
||||||
|
* Rule: a topic is finished when it has a core set (`set:core` sub-issues) and
|
||||||
|
* every one of them is closed. Optional and deferred problems are extra credit
|
||||||
|
* and never hold a topic open. A topic with no core sub-issues is never
|
||||||
|
* finished — that would be indistinguishable from a mis-labelled problem set.
|
||||||
|
*
|
||||||
|
* Library: side-effect-free on import; every call takes the caller's github().
|
||||||
|
*/
|
||||||
|
import type { GitHub } from "./github.ts";
|
||||||
|
|
||||||
|
/** One problem sub-issue, narrowed from the sub_issues payload. */
|
||||||
|
export interface Problem {
|
||||||
|
number: number;
|
||||||
|
/** `LC <num> · <Name>`. */
|
||||||
|
title: string;
|
||||||
|
/** ISO instant the issue closed; null while it is open. */
|
||||||
|
closedAt: string | null;
|
||||||
|
labels: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Core {
|
||||||
|
/** Core sub-issues, closed and open alike, lowest number first. */
|
||||||
|
all: number[];
|
||||||
|
/** The core sub-issues still open — non-empty means the topic stays open. */
|
||||||
|
pending: number[];
|
||||||
|
/** When the last core sub-issue closed; null until the topic is finished. */
|
||||||
|
finishedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every sub-issue of a topic. The sub_issues payload carries the full issue
|
||||||
|
* objects (state + labels), so no per-problem follow-up request is needed.
|
||||||
|
*/
|
||||||
|
export async function readProblems(gh: GitHub, topic: number): Promise<Problem[]> {
|
||||||
|
const problems: Problem[] = [];
|
||||||
|
for await (const raw of gh.list(`/repos/${gh.repo}/issues/${topic}/sub_issues`)) {
|
||||||
|
if (!raw || typeof raw !== "object") continue;
|
||||||
|
if (!("number" in raw) || typeof raw.number !== "number") continue;
|
||||||
|
if (!("title" in raw) || typeof raw.title !== "string") continue;
|
||||||
|
if (!("state" in raw) || typeof raw.state !== "string") continue;
|
||||||
|
if (!("labels" in raw) || !Array.isArray(raw.labels)) continue;
|
||||||
|
const closedAt =
|
||||||
|
raw.state === "closed" && "closed_at" in raw && typeof raw.closed_at === "string"
|
||||||
|
? raw.closed_at
|
||||||
|
: null;
|
||||||
|
const labels = raw.labels.flatMap((label: unknown) =>
|
||||||
|
label && typeof label === "object" && "name" in label && typeof label.name === "string"
|
||||||
|
? [label.name]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
problems.push({ number: raw.number, title: raw.title, closedAt, labels });
|
||||||
|
}
|
||||||
|
return problems.sort((a, b) => a.number - b.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The value of a `prefix:value` label (`set:` → `core`), if the problem has one. */
|
||||||
|
export function labelValue(problem: Problem, prefix: string): string | undefined {
|
||||||
|
return problem.labels.find((l) => l.startsWith(prefix))?.slice(prefix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coreSet(problems: Problem[]): Core {
|
||||||
|
const core = problems.filter((p) => labelValue(p, "set:") === "core");
|
||||||
|
const pending = core.filter((p) => !p.closedAt).map((p) => p.number);
|
||||||
|
// ISO instants from the API share one format, so string order is time order.
|
||||||
|
const finishedAt =
|
||||||
|
core.length && !pending.length
|
||||||
|
? core.map((p) => p.closedAt!).reduce((a, b) => (a > b ? a : b))
|
||||||
|
: null;
|
||||||
|
return { all: core.map((p) => p.number), pending, finishedAt };
|
||||||
|
}
|
||||||
@@ -1,13 +1,64 @@
|
|||||||
import { defineConfig } from "blume";
|
import { defineConfig } from "blume";
|
||||||
|
import { filesystem, githubReleases } from "blume/sources";
|
||||||
|
import redirects from "./redirects";
|
||||||
|
|
||||||
|
// Blume's defaults (`inter`, `ibm-plex-mono`) are fetched from fonts.gstatic.com
|
||||||
|
// on every build, which has timed out on the Pages runner and, fully offline,
|
||||||
|
// silently ships no fonts at all. The same two families from Fontsource, as
|
||||||
|
// local files, keep the build network-free. Blume resolves `src` from this
|
||||||
|
// directory; the workspace links these packages under ./node_modules.
|
||||||
|
const inter = {
|
||||||
|
name: "Inter",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
src: "./node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2",
|
||||||
|
weight: "100..900",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const plexMono = {
|
||||||
|
name: "IBM Plex Mono",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2",
|
||||||
|
weight: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2",
|
||||||
|
weight: 500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
title: "Leetcode",
|
title: "prdlk",
|
||||||
description: "Documentation for LeetCode solutions",
|
description: "Documentation for my LeetCode solutions",
|
||||||
|
logo: "./public/logo.svg",
|
||||||
deployment: {
|
deployment: {
|
||||||
site: "https://prdlk.github.io",
|
site: "https://lc.prd.lk",
|
||||||
base: "/leetcode",
|
|
||||||
},
|
},
|
||||||
|
github: {
|
||||||
|
owner: "prdlk",
|
||||||
|
repo: "leetcode",
|
||||||
|
},
|
||||||
|
lastModified: "git",
|
||||||
|
redirects,
|
||||||
content: {
|
content: {
|
||||||
root: "content",
|
sources: [
|
||||||
|
filesystem({ root: "content" }),
|
||||||
|
githubReleases({
|
||||||
|
prefix: "changelog",
|
||||||
|
owner: "prdlk",
|
||||||
|
repo: "leetcode",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
fonts: { display: inter, body: inter, mono: plexMono },
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
sidebar: {
|
||||||
|
display: "flat", // "flat" | "group" | "page"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Project MDX components, available on every page with no import. Static
|
||||||
|
* `.astro` files ship no framework JS (a custom element carries any
|
||||||
|
* behaviour); interactive React lives in `islands/` instead.
|
||||||
|
*/
|
||||||
|
import { defineComponents } from "blume";
|
||||||
|
|
||||||
|
export default defineComponents({
|
||||||
|
mdx: {
|
||||||
|
Carousel: "./components/Carousel.astro",
|
||||||
|
CarouselImage: "./components/CarouselImage.astro",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* A horizontal carousel of <CarouselImage> slides, built the way Blume's own
|
||||||
|
* content components are: static markup, a custom element for the behaviour,
|
||||||
|
* no framework. The track is a CSS scroll-snap strip (so it also swipes on
|
||||||
|
* touch and the browser does the animation); prev/next sit outside it and
|
||||||
|
* the play toggle sits below with a "n / N" counter.
|
||||||
|
*
|
||||||
|
* `time` is seconds per slide while playing (default 1.25); `autoplay` starts
|
||||||
|
* playing on load. Play wraps around, pauses whenever the tab is hidden, and
|
||||||
|
* never starts on its own for a reader who prefers reduced motion — the
|
||||||
|
* button still works for them. Manual navigation (buttons, arrow keys, swipe)
|
||||||
|
* does not stop playback; it only restarts the interval so a step is never
|
||||||
|
* doubled.
|
||||||
|
*/
|
||||||
|
import Icon from "blume/components/Icon.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Seconds each slide stays while playing. */
|
||||||
|
time?: number;
|
||||||
|
autoplay?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { time = 1.25, autoplay = false } = Astro.props;
|
||||||
|
|
||||||
|
const buttonClass =
|
||||||
|
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border bg-background text-muted-foreground transition-colors hover:border-foreground hover:text-foreground disabled:cursor-default disabled:opacity-40";
|
||||||
|
---
|
||||||
|
|
||||||
|
<blume-carousel
|
||||||
|
class="not-prose my-6 block"
|
||||||
|
data-time={String(time)}
|
||||||
|
data-autoplay={autoplay ? "" : undefined}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button class={buttonClass} data-prev type="button" aria-label="Previous slide">
|
||||||
|
<Icon class="rtl:-scale-x-100" name="chevron-left" size={18} />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="flex min-w-0 flex-1 snap-x snap-mandatory overflow-x-auto scroll-smooth rounded-blume [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
|
data-track
|
||||||
|
tabindex="0"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<button class={buttonClass} data-next type="button" aria-label="Next slide">
|
||||||
|
<Icon class="rtl:-scale-x-100" name="chevron-right" size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex items-center justify-center gap-3">
|
||||||
|
<button class={buttonClass} data-play type="button" aria-label="Play" aria-pressed="false">
|
||||||
|
<Icon class="[[aria-pressed=true]_&]:hidden" name="play" size={16} />
|
||||||
|
<Icon class="[[aria-pressed=false]_&]:hidden" name="pause" size={16} />
|
||||||
|
</button>
|
||||||
|
<span class="text-muted-foreground text-xs tabular-nums leading-5" data-count></span>
|
||||||
|
</div>
|
||||||
|
</blume-carousel>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class BlumeCarousel extends HTMLElement {
|
||||||
|
#track!: HTMLElement;
|
||||||
|
#play!: HTMLButtonElement;
|
||||||
|
#count!: HTMLElement;
|
||||||
|
#timer: number | null = null;
|
||||||
|
#index = 0;
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.#track = this.querySelector("[data-track]")!;
|
||||||
|
this.#play = this.querySelector("[data-play]")!;
|
||||||
|
this.#count = this.querySelector("[data-count]")!;
|
||||||
|
const prev = this.querySelector<HTMLButtonElement>("[data-prev]")!;
|
||||||
|
const next = this.querySelector<HTMLButtonElement>("[data-next]")!;
|
||||||
|
|
||||||
|
const slides = this.#slides().length;
|
||||||
|
if (slides < 2) {
|
||||||
|
for (const b of [prev, next, this.#play]) b.disabled = true;
|
||||||
|
}
|
||||||
|
this.#render();
|
||||||
|
|
||||||
|
prev.addEventListener("click", () => this.#step(-1));
|
||||||
|
next.addEventListener("click", () => this.#step(1));
|
||||||
|
this.#play.addEventListener("click", () => (this.#timer === null ? this.#start() : this.#stop()));
|
||||||
|
this.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "ArrowLeft") this.#step(-1);
|
||||||
|
else if (event.key === "ArrowRight") this.#step(1);
|
||||||
|
else return;
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
// Swipes and trackpad scrolls move the track without #step; follow them
|
||||||
|
// once the scroll settles. Settle-only, not per event: a long smooth
|
||||||
|
// scroll from #step passes over intermediate slides and must not be
|
||||||
|
// read back as the reader's position mid-flight.
|
||||||
|
let settle: number | undefined;
|
||||||
|
this.#track.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
() => {
|
||||||
|
window.clearTimeout(settle);
|
||||||
|
settle = window.setTimeout(() => this.#syncFromScroll(), 150);
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
document.addEventListener("visibilitychange", this.#onVisibility);
|
||||||
|
|
||||||
|
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
if (this.hasAttribute("data-autoplay") && slides > 1 && !reduced) this.#start();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#stop();
|
||||||
|
document.removeEventListener("visibilitychange", this.#onVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
#slides(): HTMLElement[] {
|
||||||
|
return [...this.#track.querySelectorAll<HTMLElement>(":scope > blume-carousel-image")];
|
||||||
|
}
|
||||||
|
|
||||||
|
#step(delta: number) {
|
||||||
|
const slides = this.#slides();
|
||||||
|
this.#index = (this.#index + delta + slides.length) % slides.length;
|
||||||
|
slides[this.#index]!.scrollIntoView({ block: "nearest", inline: "start", behavior: "smooth" });
|
||||||
|
this.#render();
|
||||||
|
if (this.#timer !== null) this.#start(); // restart the interval from this slide
|
||||||
|
}
|
||||||
|
|
||||||
|
#syncFromScroll() {
|
||||||
|
const at = Math.round(this.#track.scrollLeft / this.#track.clientWidth);
|
||||||
|
if (at !== this.#index && Number.isFinite(at)) {
|
||||||
|
this.#index = Math.min(Math.max(at, 0), this.#slides().length - 1);
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#start() {
|
||||||
|
this.#stop();
|
||||||
|
const ms = Number(this.dataset.time) * 1000;
|
||||||
|
this.#timer = window.setInterval(() => this.#step(1), ms > 0 ? ms : 1250);
|
||||||
|
this.#play.setAttribute("aria-pressed", "true");
|
||||||
|
this.#play.setAttribute("aria-label", "Pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
#stop() {
|
||||||
|
if (this.#timer !== null) window.clearInterval(this.#timer);
|
||||||
|
this.#timer = null;
|
||||||
|
this.#play.setAttribute("aria-pressed", "false");
|
||||||
|
this.#play.setAttribute("aria-label", "Play");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A background tab keeps its timers but not its layout; resume on return.
|
||||||
|
#wasPlaying = false;
|
||||||
|
#onVisibility = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.#wasPlaying = this.#timer !== null;
|
||||||
|
this.#stop();
|
||||||
|
} else if (this.#wasPlaying) this.#start();
|
||||||
|
};
|
||||||
|
|
||||||
|
#render() {
|
||||||
|
const slides = this.#slides();
|
||||||
|
this.#count.textContent = `${this.#index + 1} / ${slides.length}`;
|
||||||
|
slides.forEach((s, i) => s.setAttribute("aria-hidden", i === this.#index ? "false" : "true"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customElements.get("blume-carousel")) {
|
||||||
|
customElements.define("blume-carousel", BlumeCarousel);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* One slide of a <Carousel>: Blume's own <Frame> around an <img>, so a slide
|
||||||
|
* is pixel-identical to a framed image elsewhere on the site — `alt` doubles
|
||||||
|
* as the frame's hint line and `caption` is Markdown, exactly as in Frame.
|
||||||
|
*
|
||||||
|
* `src` accepts what MDX authors already write for images: a URL, a
|
||||||
|
* root-relative `/file.png` (served from public/, prefixed with the deploy
|
||||||
|
* base), or the page-relative `../../public/file.png` form. A relative path
|
||||||
|
* that does not pass through public/ has nothing to serve it, so it is a
|
||||||
|
* build error rather than a silent 404.
|
||||||
|
*/
|
||||||
|
import Frame from "blume/components/content/Frame.astro";
|
||||||
|
import { withBase } from "blume/components/islands/base-path.ts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
caption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { src, alt, caption } = Astro.props;
|
||||||
|
|
||||||
|
const resolveSrc = (value: string): string => {
|
||||||
|
if (URL.canParse(value)) return value;
|
||||||
|
if (value.startsWith("/")) return withBase(value);
|
||||||
|
const at = value.indexOf("public/");
|
||||||
|
if (at === -1) {
|
||||||
|
throw new Error(
|
||||||
|
`<CarouselImage src="${value}">: a relative src must point into public/ (or use /file.png)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return withBase(`/${value.slice(at + "public/".length)}`);
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<blume-carousel-image
|
||||||
|
class="block w-full shrink-0 snap-center [&_blume-frame]:my-0"
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
>
|
||||||
|
<Frame caption={caption} hint={alt}>
|
||||||
|
<img alt={alt ?? ""} src={resolveSrc(src)} loading="lazy" draggable="false" />
|
||||||
|
</Frame>
|
||||||
|
</blume-carousel-image>
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '153. Find Minimum in Rotated Sorted Array'
|
title: '153. Find Minimum in Rotated Sorted Array'
|
||||||
description: 'Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:'
|
description: 'Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:'
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Find Minimum in Rotated Sorted Array'
|
label: 'Find Minimum in Rotated Sorted Array'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '33. Search in Rotated Sorted Array'
|
title: '33. Search in Rotated Sorted Array'
|
||||||
description: There is an integer array nums sorted in ascending order (with distinct values)
|
description: There is an integer array nums sorted in ascending order (with distinct values)
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Search in Rotated Sorted Array'
|
label: 'Search in Rotated Sorted Array'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '35. Search Insert Position'
|
title: '35. Search Insert Position'
|
||||||
description: Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order
|
description: Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Search Insert Position'
|
label: 'Search Insert Position'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '704. Binary Search'
|
title: '704. Binary Search'
|
||||||
description: Given an array of integers nums which is sorted in ascending order, and an integer target, write a function to search target in nums. If target exists, then return its index. Otherwise, return -1
|
description: Given an array of integers nums which is sorted in ascending order, and an integer target, write a function to search target in nums. If target exists, then return its index. Otherwise, return -1
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Binary Search'
|
label: 'Binary Search'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '74. Search a 2D Matrix'
|
title: '74. Search a 2D Matrix'
|
||||||
description: 'You are given an m x n integer matrix matrix with the following two properties:'
|
description: 'You are given an m x n integer matrix matrix with the following two properties:'
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Search a 2D Matrix'
|
label: 'Search a 2D Matrix'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '875. Koko Eating Bananas'
|
title: '875. Koko Eating Bananas'
|
||||||
description: Koko loves to eat bananas. There are n piles of bananas, the i^th pile has piles[i] bananas. The guards have gone and will come back in h hours
|
description: Koko loves to eat bananas. There are n piles of bananas, the i^th pile has piles[i] bananas. The guards have gone and will come back in h hours
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Koko Eating Bananas'
|
label: 'Koko Eating Bananas'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Binary Search
|
||||||
|
description: 'Half of a sorted range is removed at each step until the target is found.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Binary Search", order: 5, icon: "search", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '1. Two Sum'
|
title: '1. Two Sum'
|
||||||
description: You are given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target
|
description: You are given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Two Sum'
|
label: 'Two Sum'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '1832. Check if the Sentence Is Pangram'
|
title: '1832. Check if the Sentence Is Pangram'
|
||||||
description: A pangram is a sentence where every letter of the English alphabet appears at least once
|
description: A pangram is a sentence where every letter of the English alphabet appears at least once
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Check if the Sentence Is Pangram'
|
label: 'Check if the Sentence Is Pangram'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+3
-22
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '217. Contains Duplicate'
|
title: '217. Contains Duplicate'
|
||||||
description: Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct
|
description: Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Contains Duplicate'
|
label: 'Contains Duplicate'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
@@ -42,32 +43,12 @@ flowchart TD
|
|||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
<CodeGroup>
|
```py
|
||||||
|
|
||||||
```py Python
|
|
||||||
class Solution:
|
class Solution:
|
||||||
def containsDuplicate(self, nums: List[int]) -> bool:
|
def containsDuplicate(self, nums: List[int]) -> bool:
|
||||||
|
return len(set(nums)) != len(nums)
|
||||||
```
|
```
|
||||||
|
|
||||||
```js JavaScript
|
|
||||||
/**
|
|
||||||
* @param {number[]} nums
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
var containsDuplicate = function(nums) {
|
|
||||||
const freq = {};
|
|
||||||
for (let n of nums) {
|
|
||||||
freq[n] = (freq[n] || 0) + 1;
|
|
||||||
if (freq[n] >= 2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
## Explanation
|
## Explanation
|
||||||
|
|
||||||
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
<YouTube id="3OamzN90kPg" title="Contains Duplicate - Leetcode 217 - Python" />
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '242. Valid Anagram'
|
title: '242. Valid Anagram'
|
||||||
description: Given two strings s and t, return true if t is an anagram of s, and false otherwise
|
description: Given two strings s and t, return true if t is an anagram of s, and false otherwise
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Valid Anagram'
|
label: 'Valid Anagram'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '347. Top K Frequent Elements'
|
title: '347. Top K Frequent Elements'
|
||||||
description: Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order
|
description: Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Top K Frequent Elements'
|
label: 'Top K Frequent Elements'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '387. First Unique Character in a String'
|
title: '387. First Unique Character in a String'
|
||||||
description: Given a string s, find the first non-repeating character in it and return its index. If it does not exist, return -1
|
description: Given a string s, find the first non-repeating character in it and return its index. If it does not exist, return -1
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'First Unique Character in a String'
|
label: 'First Unique Character in a String'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '451. Sort Characters By Frequency'
|
title: '451. Sort Characters By Frequency'
|
||||||
description: Given a string s, sort it in decreasing order based on the frequency of the characters. The frequency of a character is the number of times it appears in the string
|
description: Given a string s, sort it in decreasing order based on the frequency of the characters. The frequency of a character is the number of times it appears in the string
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Sort Characters By Frequency'
|
label: 'Sort Characters By Frequency'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '49. Group Anagrams'
|
title: '49. Group Anagrams'
|
||||||
description: Given an array of strings strs, group the anagrams together. You can return the answer in any order
|
description: Given an array of strings strs, group the anagrams together. You can return the answer in any order
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Group Anagrams'
|
label: 'Group Anagrams'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Hash Table
|
||||||
|
description: 'A key maps to a value in constant time, which removes the need for a full scan.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Hash Table", order: 1, icon: "hash", display: "group" });
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Phase 1: Linear Structures", order: 1 });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '739. Daily Temperatures'
|
title: '739. Daily Temperatures'
|
||||||
description: Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the i^th day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead
|
description: Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the i^th day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Daily Temperatures'
|
label: 'Daily Temperatures'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '853. Car Fleet'
|
title: '853. Car Fleet'
|
||||||
description: There are n cars at given miles away from the starting mile 0, traveling to reach the mile target
|
description: There are n cars at given miles away from the starting mile 0, traveling to reach the mile target
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Car Fleet'
|
label: 'Car Fleet'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Monotonic Stack
|
||||||
|
description: 'A stack keeps its items in sorted order, which finds the next larger or smaller item in one pass.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Monotonic Stack", order: 7, icon: "trending-up", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '238. Product of Array Except Self'
|
title: '238. Product of Array Except Self'
|
||||||
description: Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]
|
description: Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Product of Array Except Self'
|
label: 'Product of Array Except Self'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '303. Range Sum Query - Immutable'
|
title: '303. Range Sum Query - Immutable'
|
||||||
description: Given an integer array nums, handle multiple queries of the following type
|
description: Given an integer array nums, handle multiple queries of the following type
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Range Sum Query - Immutable'
|
label: 'Range Sum Query - Immutable'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '560. Subarray Sum Equals K'
|
title: '560. Subarray Sum Equals K'
|
||||||
description: Given an array of integers nums and an integer k, return the total number of subarrays whose sum equals to k
|
description: Given an array of integers nums and an integer k, return the total number of subarrays whose sum equals to k
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Subarray Sum Equals K'
|
label: 'Subarray Sum Equals K'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Prefix Sum
|
||||||
|
description: 'A running total is stored at each index, so the sum of any range is one subtraction.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Prefix Sum", order: 2, icon: "sigma", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '121. Best Time to Buy and Sell Stock'
|
title: '121. Best Time to Buy and Sell Stock'
|
||||||
description: You are given an array prices where prices[i] is the price of a given stock on the i^th day
|
description: You are given an array prices where prices[i] is the price of a given stock on the i^th day
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Best Time to Buy and Sell Stock'
|
label: 'Best Time to Buy and Sell Stock'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '3. Longest Substring Without Repeating Characters'
|
title: '3. Longest Substring Without Repeating Characters'
|
||||||
description: Given a string s, find the length of the longest substring without duplicate characters
|
description: Given a string s, find the length of the longest substring without duplicate characters
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Longest Substring Without Repeating Characters'
|
label: 'Longest Substring Without Repeating Characters'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '424. Longest Repeating Character Replacement'
|
title: '424. Longest Repeating Character Replacement'
|
||||||
description: You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times
|
description: You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Longest Repeating Character Replacement'
|
label: 'Longest Repeating Character Replacement'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '567. Permutation in String'
|
title: '567. Permutation in String'
|
||||||
description: Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise
|
description: Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Permutation in String'
|
label: 'Permutation in String'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+3
-3
@@ -55,7 +55,7 @@ Without an invariant, shrinking the window becomes arbitrary. You no longer know
|
|||||||
The window size never changes. The main task is updating the window state efficiently when one element enters and one leaves.
|
The window size never changes. The main task is updating the window state efficiently when one element enters and one leaves.
|
||||||
:::
|
:::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 2. Flexible-size windows with a validity rule
|
### 2. Flexible-size windows with a validity rule
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ The window size never changes. The main task is updating the window state effici
|
|||||||
Here the window expands until invalid, then shrinks until valid again.
|
Here the window expands until invalid, then shrinks until valid again.
|
||||||
:::
|
:::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### 3. Frequency-tracking windows
|
### 3. Frequency-tracking windows
|
||||||
@@ -72,7 +72,7 @@ Here the window expands until invalid, then shrinks until valid again.
|
|||||||
The difficulty comes from keeping the bookkeeping correct as characters enter and leave.
|
The difficulty comes from keeping the bookkeeping correct as characters enter and leave.
|
||||||
:::
|
:::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## How Sliding Window Differs From Other Two-Pointer Families
|
## How Sliding Window Differs From Other Two-Pointer Families
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Sliding Window", order: 4, icon: "app-window", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '150. Evaluate Reverse Polish Notation'
|
title: '150. Evaluate Reverse Polish Notation'
|
||||||
description: You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation
|
description: You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Evaluate Reverse Polish Notation'
|
label: 'Evaluate Reverse Polish Notation'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '155. Min Stack'
|
title: '155. Min Stack'
|
||||||
description: Design a stack that supports push, pop, top, and retrieving the minimum element in constant time
|
description: Design a stack that supports push, pop, top, and retrieving the minimum element in constant time
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Min Stack'
|
label: 'Min Stack'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '20. Valid Parentheses'
|
title: '20. Valid Parentheses'
|
||||||
description: Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid
|
description: Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Valid Parentheses'
|
label: 'Valid Parentheses'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Stack
|
||||||
|
description: 'Items are added and removed from one end, so the last item added is the first item removed.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Stack", order: 6, icon: "layers", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '11. Container With Most Water'
|
title: '11. Container With Most Water'
|
||||||
description: You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the i^th line are (i, 0) and (i, height[i])
|
description: You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the i^th line are (i, 0) and (i, height[i])
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Container With Most Water'
|
label: 'Container With Most Water'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '125. Valid Palindrome'
|
title: '125. Valid Palindrome'
|
||||||
description: A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers
|
description: A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Valid Palindrome'
|
label: 'Valid Palindrome'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '15. 3Sum'
|
title: '15. 3Sum'
|
||||||
description: Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0
|
description: Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: '3Sum'
|
label: '3Sum'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '167. Two Sum II - Input Array Is Sorted'
|
title: '167. Two Sum II - Input Array Is Sorted'
|
||||||
description: Given a 1-indexed array of integers numbers that is already sorted in non-decreasing order, find two numbers such that they add up to a specific target number. Let these two numbers be numbers[index1] and numbers[index2] where 1 <= index1 < index2 <= numbers.length
|
description: Given a 1-indexed array of integers numbers that is already sorted in non-decreasing order, find two numbers such that they add up to a specific target number. Let these two numbers be numbers[index1] and numbers[index2] where 1 <= index1 < index2 <= numbers.length
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Two Sum II - Input Array Is Sorted'
|
label: 'Two Sum II - Input Array Is Sorted'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '344. Reverse String'
|
title: '344. Reverse String'
|
||||||
description: Write a function that reverses a string. The input string is given as an array of characters s
|
description: Write a function that reverses a string. The input string is given as an array of characters s
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Reverse String'
|
label: 'Reverse String'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '392. Is Subsequence'
|
title: '392. Is Subsequence'
|
||||||
description: Given two strings s and t, return true if s is a subsequence of t, or false otherwise
|
description: Given two strings s and t, return true if s is a subsequence of t, or false otherwise
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Is Subsequence'
|
label: 'Is Subsequence'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '977. Squares of a Sorted Array'
|
title: '977. Squares of a Sorted Array'
|
||||||
description: Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order
|
description: Given an integer array nums sorted in non-decreasing order, return an array of the squares of each number sorted in non-decreasing order
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Squares of a Sorted Array'
|
label: 'Squares of a Sorted Array'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Two Pointers
|
||||||
|
description: 'Two indices move through the data from different positions and meet at the answer.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Two Pointers", order: 3, icon: "arrow-left-right", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '146. LRU Cache'
|
title: '146. LRU Cache'
|
||||||
description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache
|
description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'LRU Cache'
|
label: 'LRU Cache'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '2. Add Two Numbers'
|
title: '2. Add Two Numbers'
|
||||||
description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list
|
description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Add Two Numbers'
|
label: 'Add Two Numbers'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Hybrid Structures
|
||||||
|
description: 'Two structures are combined, such as a hash map and a linked list, to get the best properties of each.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
import { defineMeta } from "blume";
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
export default defineMeta({ title: "Hybrid Structures", order: 9 });
|
export default defineMeta({ title: "Hybrid Structures", order: 9, icon: "combine", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '141. Linked List Cycle'
|
title: '141. Linked List Cycle'
|
||||||
description: Given head, the head of a linked list, determine if the linked list has a cycle in it
|
description: Given head, the head of a linked list, determine if the linked list has a cycle in it
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Linked List Cycle'
|
label: 'Linked List Cycle'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '206. Reverse Linked List'
|
title: '206. Reverse Linked List'
|
||||||
description: Given the head of a singly linked list, reverse the list, and return the reversed list
|
description: Given the head of a singly linked list, reverse the list, and return the reversed list
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Reverse Linked List'
|
label: 'Reverse Linked List'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '21. Merge Two Sorted Lists'
|
title: '21. Merge Two Sorted Lists'
|
||||||
description: You are given the heads of two sorted linked lists list1 and list2
|
description: You are given the heads of two sorted linked lists list1 and list2
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Merge Two Sorted Lists'
|
label: 'Merge Two Sorted Lists'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Linked List
|
||||||
|
description: 'Each node points to the next node, and the pointers give the path through the data.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Linked List", order: 8, icon: "link", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '48. Rotate Image'
|
title: '48. Rotate Image'
|
||||||
description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise)
|
description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise)
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Rotate Image'
|
label: 'Rotate Image'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '54. Spiral Matrix'
|
title: '54. Spiral Matrix'
|
||||||
description: Given an m x n matrix, return all elements of the matrix in spiral order
|
description: Given an m x n matrix, return all elements of the matrix in spiral order
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Spiral Matrix'
|
label: 'Spiral Matrix'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '73. Set Matrix Zeroes'
|
title: '73. Set Matrix Zeroes'
|
||||||
description: Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0's
|
description: Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0's
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Set Matrix Zeroes'
|
label: 'Set Matrix Zeroes'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Matrix Index Math
|
||||||
|
description: 'A two-dimensional grid is read as a flat array through a formula, and the neighbors are found by arithmetic.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
import { defineMeta } from "blume";
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
export default defineMeta({ title: "Matrix Index Math", order: 10 });
|
export default defineMeta({ title: "Matrix Index Math", order: 10, icon: "grid-3x3", display: "group" });
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Phase 2: Nodal & Grid", order: 2 });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '230. Kth Smallest Element in a BST'
|
title: '230. Kth Smallest Element in a BST'
|
||||||
description: Given the root of a binary search tree, and an integer k, return the k^th smallest value (1-indexed) of all the values of the nodes in the tree
|
description: Given the root of a binary search tree, and an integer k, return the k^th smallest value (1-indexed) of all the values of the nodes in the tree
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Kth Smallest Element in a BST'
|
label: 'Kth Smallest Element in a BST'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '235. Lowest Common Ancestor of a Binary Search Tree'
|
title: '235. Lowest Common Ancestor of a Binary Search Tree'
|
||||||
description: Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST
|
description: Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Lowest Common Ancestor of a Binary Search Tree'
|
label: 'Lowest Common Ancestor of a Binary Search Tree'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '98. Validate Binary Search Tree'
|
title: '98. Validate Binary Search Tree'
|
||||||
description: Given the root of a binary tree, determine if it is a valid binary search tree (BST)
|
description: Given the root of a binary tree, determine if it is a valid binary search tree (BST)
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Validate Binary Search Tree'
|
label: 'Validate Binary Search Tree'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Binary Search Tree
|
||||||
|
description: 'Each node holds a value that is larger than its left subtree and smaller than its right subtree.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
import { defineMeta } from "blume";
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
export default defineMeta({ title: "Binary Search Tree", order: 13 });
|
export default defineMeta({ title: "Binary Search Tree", order: 13, icon: "git-fork", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '215. Kth Largest Element in an Array'
|
title: '215. Kth Largest Element in an Array'
|
||||||
description: Given an integer array nums and an integer k, return the k^th largest element in the array
|
description: Given an integer array nums and an integer k, return the k^th largest element in the array
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Kth Largest Element in an Array'
|
label: 'Kth Largest Element in an Array'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '23. Merge k Sorted Lists'
|
title: '23. Merge k Sorted Lists'
|
||||||
description: You are given an array of k linked-lists lists, each linked-list is sorted in ascending order
|
description: You are given an array of k linked-lists lists, each linked-list is sorted in ascending order
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Merge k Sorted Lists'
|
label: 'Merge k Sorted Lists'
|
||||||
badge: 'Hard'
|
badge: 'Hard'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '973. K Closest Points to Origin'
|
title: '973. K Closest Points to Origin'
|
||||||
description: Given an array of points where points[i] = [xi, yi] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0)
|
description: Given an array of points where points[i] = [xi, yi] represents a point on the X-Y plane and an integer k, return the k closest points to the origin (0, 0)
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'K Closest Points to Origin'
|
label: 'K Closest Points to Origin'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Heap / Priority Queue
|
||||||
|
description: 'The largest or smallest item is always at the top, and it is removed in logarithmic time.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
import { defineMeta } from "blume";
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
export default defineMeta({ title: "Heap / Priority Queue", order: 14 });
|
export default defineMeta({ title: "Heap / Priority Queue", order: 14, icon: "triangle", display: "group" });
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Phase 3: Hierarchical", order: 3 });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '102. Binary Tree Level Order Traversal'
|
title: '102. Binary Tree Level Order Traversal'
|
||||||
description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level)
|
description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level)
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Binary Tree Level Order Traversal'
|
label: 'Binary Tree Level Order Traversal'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '1448. Count Good Nodes in Binary Tree'
|
title: '1448. Count Good Nodes in Binary Tree'
|
||||||
description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X
|
description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Count Good Nodes in Binary Tree'
|
label: 'Count Good Nodes in Binary Tree'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '199. Binary Tree Right Side View'
|
title: '199. Binary Tree Right Side View'
|
||||||
description: Given the root of a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom
|
description: Given the root of a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Binary Tree Right Side View'
|
label: 'Binary Tree Right Side View'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Tree BFS
|
||||||
|
description: 'All nodes at one level are visited before the nodes at the next level.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Tree BFS", order: 12, icon: "network", display: "group" });
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '104. Maximum Depth of Binary Tree'
|
title: '104. Maximum Depth of Binary Tree'
|
||||||
description: Given the root of a binary tree, return its maximum depth
|
description: Given the root of a binary tree, return its maximum depth
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Maximum Depth of Binary Tree'
|
label: 'Maximum Depth of Binary Tree'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '226. Invert Binary Tree'
|
title: '226. Invert Binary Tree'
|
||||||
description: Given the root of a binary tree, invert the tree, and return its root
|
description: Given the root of a binary tree, invert the tree, and return its root
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Invert Binary Tree'
|
label: 'Invert Binary Tree'
|
||||||
badge: 'Easy'
|
badge: 'Easy'
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: '236. Lowest Common Ancestor of a Binary Tree'
|
title: '236. Lowest Common Ancestor of a Binary Tree'
|
||||||
description: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree
|
description: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree
|
||||||
|
icon: dot
|
||||||
sidebar:
|
sidebar:
|
||||||
label: 'Lowest Common Ancestor of a Binary Tree'
|
label: 'Lowest Common Ancestor of a Binary Tree'
|
||||||
badge: 'Medium'
|
badge: 'Medium'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Tree DFS
|
||||||
|
description: 'Each branch is followed to its end before the next branch is examined.'
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { defineMeta } from "blume";
|
||||||
|
|
||||||
|
export default defineMeta({ title: "Tree DFS", order: 11, icon: "folder-tree", display: "group" });
|
||||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user