From 7b22e3f5aa23424535d44b919d99130d9efb7fd0 Mon Sep 17 00:00:00 2001
From: Charliechen114514 <725610365@qq.com>
Date: Wed, 7 Oct 2026 12:40:30 +0800
Subject: [PATCH] feat: Add bookmarks locally
---
documents/bookmarks.md | 6 +
documents/en/bookmarks.md | 9 +
.../exercises/00-thread-lifecycle.md | 14 +-
.../exercises/01-bounded-queue.md | 6 +-
.../exercises/00-thread-lifecycle.md | 12 +-
.../exercises/01-bounded-queue.md | 4 +-
scripts/build.ts | 10 +-
scripts/check_quality.py | 2 +-
scripts/validate_frontmatter.py | 4 +-
site/.vitepress/config/nav.ts | 2 +
.../theme/components/ArticleNotes.vue | 249 ++++++++++++
.../theme/components/BookmarkButton.vue | 83 ++++
.../theme/components/BookmarkList.vue | 354 ++++++++++++++++++
.../theme/components/NoteCapture.vue | 149 ++++++++
site/.vitepress/theme/composables/noteDom.ts | 90 +++++
.../theme/composables/readingKeys.ts | 57 +++
.../theme/composables/useBookmarks.ts | 232 ++++++++++++
site/.vitepress/theme/composables/useNotes.ts | 133 +++++++
site/.vitepress/theme/custom.css | 10 +
site/.vitepress/theme/index.ts | 15 +-
site/.vitepress/theme/utils/noteText.test.ts | 49 +++
site/.vitepress/theme/utils/noteText.ts | 59 +++
22 files changed, 1522 insertions(+), 27 deletions(-)
create mode 100644 documents/bookmarks.md
create mode 100644 documents/en/bookmarks.md
create mode 100644 site/.vitepress/theme/components/ArticleNotes.vue
create mode 100644 site/.vitepress/theme/components/BookmarkButton.vue
create mode 100644 site/.vitepress/theme/components/BookmarkList.vue
create mode 100644 site/.vitepress/theme/components/NoteCapture.vue
create mode 100644 site/.vitepress/theme/composables/noteDom.ts
create mode 100644 site/.vitepress/theme/composables/readingKeys.ts
create mode 100644 site/.vitepress/theme/composables/useBookmarks.ts
create mode 100644 site/.vitepress/theme/composables/useNotes.ts
create mode 100644 site/.vitepress/theme/utils/noteText.test.ts
create mode 100644 site/.vitepress/theme/utils/noteText.ts
diff --git a/documents/bookmarks.md b/documents/bookmarks.md
new file mode 100644
index 000000000..b2398f6ae
--- /dev/null
+++ b/documents/bookmarks.md
@@ -0,0 +1,6 @@
+---
+title: "书签"
+description: "收藏的文章与摘录的便签都在这里——续读、跳回原文原处、导出导入备份"
+---
+
+
diff --git a/documents/en/bookmarks.md b/documents/en/bookmarks.md
new file mode 100644
index 000000000..a7b1dee35
--- /dev/null
+++ b/documents/en/bookmarks.md
@@ -0,0 +1,9 @@
+---
+title: "Bookmarks"
+description: "Your saved articles and highlighted notes live here — resume reading, jump back to the exact passage, export/import a backup"
+translation:
+ source: documents/bookmarks.md
+ source_hash: 964ebc328726e597852aaee288a2c8c4a350ea148065b1639ce3e2cc89b46457
+---
+
+
diff --git a/documents/en/vol5-concurrency/exercises/00-thread-lifecycle.md b/documents/en/vol5-concurrency/exercises/00-thread-lifecycle.md
index 9f3251dc2..4305845a7 100644
--- a/documents/en/vol5-concurrency/exercises/00-thread-lifecycle.md
+++ b/documents/en/vol5-concurrency/exercises/00-thread-lifecycle.md
@@ -21,14 +21,14 @@ related:
- "Thread Ownership and RAII"
translation:
source: documents/vol5-concurrency/exercises/00-thread-lifecycle.md
- source_hash: ff4f57476dec5b5d89b2ce4d45333b7aa37f6a7714a8be66b5ffee096c7fea97
+ source_hash: a0dd59669eea75ce22481a7d0419d3ff2c5e84f4e216a933a830c20cbe1cdebb
translated_at: '2026-09-26T09:17:12+00:00'
engine: anthropic
token_count: 11800
---
# Lab 0: Thread Lifecycle
-> The runnable project that goes with this Lab lives at [`code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/). Expect roughly **4–6 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).
+> The runnable project that goes with this Lab lives at `code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`. Expect roughly **4–6 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).
## Objectives
@@ -71,7 +71,7 @@ templates/lab0_thread_lifecycle/
└── test_milestone1.cpp … test_milestone4.cpp
```
-Build instructions for the entire `vol5-labs/` directory, plus the dogfooding feedback process, live in [`vol5-labs/README.md`](../../../../code/volumn_codes/vol5-labs/README.md). Read it first.
+Build instructions for the entire `vol5-labs/` directory, plus the dogfooding feedback process, live in `vol5-labs/README.md`. Read it first.
First build (requires internet access; FetchContent pulls Catch2 v3):
@@ -181,7 +181,7 @@ For statistics, start with the simplest thing possible: global `std::atomic **Don't let the tests fool you**: `test_milestone2` only tests the `JoiningThread` class itself (decoupled from `FileScanner`) and **does not check whether `scan()` actually uses it**. So even with `JoiningThread` implemented and every test green, if `scan()` still uses raw `std::thread`s plus a manual `join()` loop, this milestone has not truly been completed. **The real acceptance criteria: no manual `join()` loop visible in `scan()`, and the thread container is `std::vector`.**
-[`test/test_milestone2.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone2.cpp) only tests `JoiningThread` itself (decoupled from `FileScanner`), covering four scenarios: automatic join at scope exit, all workers still joined on an exception path, ownership transferred by move, and `vector` destruction joining everything. Pay attention to the exception-path one:
+`test/test_milestone2.cpp` only tests `JoiningThread` itself (decoupled from `FileScanner`), covering four scenarios: automatic join at scope exit, all workers still joined on an exception path, ownership transferred by move, and `vector` destruction joining everything. Pay attention to the exception-path one:
```cpp
TEST_CASE("MS2: exception path still joins all workers", "[lab0][milestone2]") {
@@ -272,7 +272,7 @@ In MS1 we passed the file path list to workers by value — which is in fact alr
### Verification
-[`test/test_milestone3.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone3.cpp) verifies: non-divisible splits still cover all files (30 files / 8 workers), prime file counts (17 files) lose nothing under any split, and move-only types (`unique_ptr`) can be passed into threads safely. Take the prime case:
+`test/test_milestone3.cpp` verifies: non-divisible splits still cover all files (30 files / 8 workers), prime file counts (17 files) lose nothing under any split, and move-only types (`unique_ptr`) can be passed into threads safely. Take the prime case:
```cpp
TEST_CASE("MS3: prime file count covered by any worker count", "[lab0][milestone3]") {
@@ -330,7 +330,7 @@ One more small point: the `worker_id` in `results[worker_id]` must be unique per
> **Don't let the tests fool you**: `test_milestone4` only checks that the result numbers are right (matching the single-threaded run); it **does not check whether the statistics are genuinely "local per worker"**. So even with every test green, if `scan()` still does its statistics through a shared `mutex`/`atomic`, you are really still at MS1 and this milestone has not truly been completed. **The real acceptance criteria: no locks and no shared atomics in `scan()`; statistics go through independent `results[worker_id]` slots plus main-thread aggregation.**
-[`test/test_milestone4.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone4.cpp) verifies: a multi-threaded scan's results are **exactly identical** to a single-threaded file-by-file scan (all three of file count, byte count, and extension distribution must match), plus a stress test of 200 files / 8 workers. Key assertion:
+`test/test_milestone4.cpp` verifies: a multi-threaded scan's results are **exactly identical** to a single-threaded file-by-file scan (all three of file count, byte count, and extension distribution must match), plus a stress test of 200 files / 8 workers. Key assertion:
```cpp
TEST_CASE("MS4: multi-threaded stats match single-threaded baseline", "[lab0][milestone4]") {
diff --git a/documents/en/vol5-concurrency/exercises/01-bounded-queue.md b/documents/en/vol5-concurrency/exercises/01-bounded-queue.md
index e1511fad0..89d4ae511 100644
--- a/documents/en/vol5-concurrency/exercises/01-bounded-queue.md
+++ b/documents/en/vol5-concurrency/exercises/01-bounded-queue.md
@@ -21,7 +21,7 @@ related:
- 'latch, barrier, and semaphore'
translation:
source: documents/vol5-concurrency/exercises/01-bounded-queue.md
- source_hash: 956f42289afc87049bb47f380040c1b6681e68a1a1c502c030dcf97530a0cb5c
+ source_hash: 2d5b3222401ff2c5b5d79f833d783a8940074148bc9588a893a31dc250cf4617
translated_at: '2026-09-26T09:22:45+00:00'
engine: anthropic
token_count: 8200
@@ -29,7 +29,7 @@ translation:
# Lab 1: Bounded Queue, Concurrent Cache and Sync Primitives
-> The runnable companion project for this lab lives at [`code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`](../../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/). Expect roughly **8–12 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).
+> The runnable companion project for this lab lives at `code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`. Expect roughly **8–12 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).
## Goal
@@ -145,7 +145,7 @@ That lambda predicate is your lifeline. If you write `not_full_.wait(lock)` (no
> **Don't be fooled by the tests**: `test_milestone1` checks "you can push and pop, FIFO order, blocking behavior, multiple producers with no loss or duplication" — **all behavior, never whether you used a predicate wait**. You could perfectly well pass the tests with a bare `wait()` (no predicate), if no spurious wakeup happens to fire. But that's a time bomb: under high concurrency or particular schedulings, it will go off. **The real acceptance criterion: every wait in `push`/`pop` is a predicate wait (`cv.wait(lock, predicate)`); there is no bare `wait()`.** Run MS4's stress test under TSan, and a bare wait will sooner or later surface as a data race or an out-of-bounds access.
-[`test/test_milestone1.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/test/test_milestone1.cpp) covers four scenarios: single push/pop, FIFO order, pop blocking until a push arrives, and multiple producers with no loss or duplication under concurrency.
+`test/test_milestone1.cpp` covers four scenarios: single push/pop, FIFO order, pop blocking until a push arrives, and multiple producers with no loss or duplication under concurrency.
## Milestone 2: close Semantics
diff --git a/documents/vol5-concurrency/exercises/00-thread-lifecycle.md b/documents/vol5-concurrency/exercises/00-thread-lifecycle.md
index ae39c5b95..033023716 100644
--- a/documents/vol5-concurrency/exercises/00-thread-lifecycle.md
+++ b/documents/vol5-concurrency/exercises/00-thread-lifecycle.md
@@ -23,7 +23,7 @@ related:
# Lab 0: Thread Lifecycle Lab
-> 本 Lab 配套可运行工程在 [`code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/)。动手工作量约 **4–6 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。
+> 本 Lab 配套可运行工程在 `code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`。动手工作量约 **4–6 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。
## 目标
@@ -66,7 +66,7 @@ templates/lab0_thread_lifecycle/
└── test_milestone1.cpp … test_milestone4.cpp
```
-整个 `vol5-labs/` 目录的构建说明和 dogfooding 反馈流程见 [`vol5-labs/README.md`](../../../code/volumn_codes/vol5-labs/README.md)。先把它读一遍。
+整个 `vol5-labs/` 目录的构建说明和 dogfooding 反馈流程见 `vol5-labs/README.md`。先把它读一遍。
第一次构建(需要联网,FetchContent 会拉取 Catch2 v3):
@@ -176,7 +176,7 @@ return 汇总
### 验证
-对应测试在 [`test/test_milestone1.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone1.cpp),覆盖三个场景:扫描收集到全部文件、空目录不崩溃、总字节数正确。关键断言:
+对应测试在 `test/test_milestone1.cpp`,覆盖三个场景:扫描收集到全部文件、空目录不崩溃、总字节数正确。关键断言:
```cpp
TEST_CASE("MS1: scan collects all files", "[lab0][milestone1]") {
@@ -223,7 +223,7 @@ Milestone 1 的手工 `join()` 有个明显问题:如果在 join 循环之前
> **别被测试骗了**:`test_milestone2` 只测 `JoiningThread` 类本身(和 `FileScanner` 解耦),**不检查 `scan()` 有没有真的用它**。所以哪怕你实现了 `JoiningThread`、测试全绿,但 `scan()` 里还是裸 `std::thread` + 手工 `join()` 循环——这个 milestone 就没真正完成。**真正的验收标准:`scan()` 里看不到手工 `join()` 循环,线程容器是 `std::vector`。**
-[`test/test_milestone2.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone2.cpp) 只测 `JoiningThread` 本身(和 `FileScanner` 解耦),覆盖四个场景:作用域结束自动 join、异常路径仍 join 全部 worker、move 转移所有权、`vector` 析构 join 全部。重点看异常路径这个:
+`test/test_milestone2.cpp` 只测 `JoiningThread` 本身(和 `FileScanner` 解耦),覆盖四个场景:作用域结束自动 join、异常路径仍 join 全部 worker、move 转移所有权、`vector` 析构 join 全部。重点看异常路径这个:
```cpp
TEST_CASE("MS2: exception path still joins all workers", "[lab0][milestone2]") {
@@ -267,7 +267,7 @@ MS1 我们把文件路径列表按值传给 worker——这其实已经是安全
### 验证
-[`test/test_milestone3.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone3.cpp) 验证:非整除分片也覆盖所有文件(30 文件 / 8 worker)、素数文件数(17 文件)任何分片都不丢、move-only 类型(`unique_ptr`)能安全传入线程。比如素数那个:
+`test/test_milestone3.cpp` 验证:非整除分片也覆盖所有文件(30 文件 / 8 worker)、素数文件数(17 文件)任何分片都不丢、move-only 类型(`unique_ptr`)能安全传入线程。比如素数那个:
```cpp
TEST_CASE("MS3: prime file count covered by any worker count", "[lab0][milestone3]") {
@@ -325,7 +325,7 @@ return total;
> **别被测试骗了**:`test_milestone4` 只验结果数值对不对(和单线程一致),**不检查统计是不是真的"每 worker 局部"**。所以哪怕测试全绿,但 `scan()` 里还在用共享 `mutex`/`atomic` 统计——你其实停在 MS1,这个 milestone 没真正完成。**真正的验收标准:`scan()` 里没有锁、没有共享 atomic,统计走 `results[worker_id]` 独立槽位 + 主线程汇总。**
-[`test/test_milestone4.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone4.cpp) 验证:多线程扫描结果与单线程逐一扫描**完全一致**(文件数、字节数、扩展名分布三项都对),外加一个 200 文件 / 8 worker 的压力测试。关键断言:
+`test/test_milestone4.cpp` 验证:多线程扫描结果与单线程逐一扫描**完全一致**(文件数、字节数、扩展名分布三项都对),外加一个 200 文件 / 8 worker 的压力测试。关键断言:
```cpp
TEST_CASE("MS4: multi-threaded stats match single-threaded baseline", "[lab0][milestone4]") {
diff --git a/documents/vol5-concurrency/exercises/01-bounded-queue.md b/documents/vol5-concurrency/exercises/01-bounded-queue.md
index 9ae0e39fd..2b64a9ca5 100644
--- a/documents/vol5-concurrency/exercises/01-bounded-queue.md
+++ b/documents/vol5-concurrency/exercises/01-bounded-queue.md
@@ -23,7 +23,7 @@ related:
# Lab 1: Bounded Queue, Concurrent Cache and Sync Primitives
-> 本 Lab 配套可运行工程在 [`code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`](../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/)。动手工作量约 **8–12 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。
+> 本 Lab 配套可运行工程在 `code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`。动手工作量约 **8–12 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。
## 目标
@@ -139,7 +139,7 @@ not_empty_.notify_one();
> **别被测试骗了**:`test_milestone1` 测的是"能放能取、FIFO、阻塞行为、多生产者不丢不重"——**全是行为,不查你用没用 predicate wait**。你完全可以用裸 `wait()`(无谓词)蒙混过测试(碰巧没触发虚假唤醒)。但那是定时炸弹:高并发或特定调度下必炸。**真正的验收标准:`push`/`pop` 的等待都是谓词 wait(`cv.wait(lock, predicate)`),没有裸 `wait()`。** 用 TSan 跑 MS4 的压力测试,裸 wait 迟早会以 data race 或越界暴露。
-[`test/test_milestone1.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/test/test_milestone1.cpp) 覆盖四个场景:单 push/pop、FIFO、pop 阻塞直到 push、多生产者并发不丢不重。
+`test/test_milestone1.cpp` 覆盖四个场景:单 push/pop、FIFO、pop 阻塞直到 push、多生产者并发不丢不重。
## Milestone 2: close 语义
diff --git a/scripts/build.ts b/scripts/build.ts
index a51bc4f64..a65915d0a 100644
--- a/scripts/build.ts
+++ b/scripts/build.ts
@@ -280,7 +280,11 @@ export default withDrawio(defineConfig({
...sharedBase,
srcDir: '${relSrc.replace(/\\/g, '/')}',
outDir: '${relOut.replace(/\\/g, '/')}',
- ignoreDeadLinks: true,
+ // 根站 srcDir 只有 index/tags/bookmarks(中英),首页正文链向各卷的入口物理不存在,
+ // 只忽略各卷挂载前缀(含 /en 侧);根站页面之间的链接(/tags /bookmarks /en/tags...)仍被死链检查覆盖。
+ ignoreDeadLinks: ${JSON.stringify([
+ ...VOLUMES.flatMap(v => [v.urlPrefix, `${v.urlPrefix}/**`, `/en${v.urlPrefix}`, `/en${v.urlPrefix}/**`]),
+ ])},
transformPageData(pageData) { applyTagsPageData(pageData); applyArticleContributors(pageData) },
title: '现代 C++ 教程',
description: '系统化的现代 C++ 教程 — 从基础入门到领域实战',
@@ -760,7 +764,7 @@ async function main() {
const rootSrcDir = join(BUILD_TMP, 'root-src')
mkdirSync(rootSrcDir, { recursive: true })
- for (const f of ['index.md', 'tags.md']) {
+ for (const f of ['index.md', 'tags.md', 'bookmarks.md']) {
const s = join(DOCUMENTS, f)
if (existsSync(s)) cpSync(s, join(rootSrcDir, f))
}
@@ -772,7 +776,7 @@ async function main() {
}
if (existsSync(join(DOCUMENTS, 'en'))) {
mkdirSync(join(rootSrcDir, 'en'), { recursive: true })
- for (const f of ['index.md', 'tags.md']) {
+ for (const f of ['index.md', 'tags.md', 'bookmarks.md']) {
const s = join(DOCUMENTS, 'en', f)
if (existsSync(s)) cpSync(s, join(rootSrcDir, 'en', f))
}
diff --git a/scripts/check_quality.py b/scripts/check_quality.py
index f29c4d94e..0b6473585 100644
--- a/scripts/check_quality.py
+++ b/scripts/check_quality.py
@@ -37,7 +37,7 @@
REQUIRED_FM_FIELDS = {'title', 'chapter', 'order'}
RECOMMENDED_FM_FIELDS = {'description', 'tags'}
-SKIP_FILENAMES = {'index.md', 'tags.md', 'README.md'}
+SKIP_FILENAMES = {'index.md', 'tags.md', 'bookmarks.md', 'README.md'}
SKIP_DIR_PARTS = {'images', 'generated'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.drawio'}
diff --git a/scripts/validate_frontmatter.py b/scripts/validate_frontmatter.py
index 787a82a01..70efa1a94 100755
--- a/scripts/validate_frontmatter.py
+++ b/scripts/validate_frontmatter.py
@@ -221,9 +221,9 @@ def run(self) -> bool:
"""Run validation on all markdown files in tutorial directory."""
md_files = list(self.tutorial_dir.rglob('*.md'))
- # Skip index.md files and tags.md (they don't need frontmatter)
+ # Skip index.md files and tags.md/bookmarks.md (site tool pages, not chapter articles)
# Also skip non-article files (e.g. images/ directory)
- skip_names = {'index.md', 'tags.md', 'README.md'}
+ skip_names = {'index.md', 'tags.md', 'bookmarks.md', 'README.md'}
skip_dir_parts = {'images'}
md_files = [
f for f in md_files
diff --git a/site/.vitepress/config/nav.ts b/site/.vitepress/config/nav.ts
index a3c7f55a9..4efd4872e 100644
--- a/site/.vitepress/config/nav.ts
+++ b/site/.vitepress/config/nav.ts
@@ -41,6 +41,7 @@ export const navZh: DefaultTheme.NavItem[] = [
{
text: '更多',
items: [
+ { text: '书签', link: '/bookmarks' },
{ text: '标签索引', link: '/tags' },
{ text: '附录', link: '/appendix/' },
{ text: '路线图', link: '/roadmap/' },
@@ -89,6 +90,7 @@ export const navEn: DefaultTheme.NavItem[] = [
{
text: 'More',
items: [
+ { text: 'Bookmarks', link: '/en/bookmarks' },
{ text: 'Tag Index', link: '/en/tags' },
{ text: 'Appendix', link: '/en/appendix/' },
{ text: 'Roadmap', link: '/en/roadmap/' },
diff --git a/site/.vitepress/theme/components/ArticleNotes.vue b/site/.vitepress/theme/components/ArticleNotes.vue
new file mode 100644
index 000000000..e2f153753
--- /dev/null
+++ b/site/.vitepress/theme/components/ArticleNotes.vue
@@ -0,0 +1,249 @@
+
+
+
+