diff --git a/README.md b/README.md
index ee9bc1397..cf7e34c74 100644
--- a/README.md
+++ b/README.md
@@ -133,12 +133,13 @@ flowchart TD
| `pnpm dev` | 启动 VitePress 开发服务器,支持热更新 |
| `pnpm build` | 生产构建,按分卷并行构建并合并搜索索引 |
| `pnpm build:single` | 使用 VitePress 单体构建 |
+| `pnpm check:links` | 检查 Markdown 与组件内部链接有效性 |
| `pnpm preview` | 预览生产构建结果 |
| `pnpm hooks:install` / `scripts/setup_precommit.sh` | 安装提交前 Git hook |
| `pnpm coverage` | 查看英文翻译覆盖率 |
| `pnpm coverage:update` | 更新 `README.md` 中的英文翻译覆盖率徽章 |
| `python3 scripts/validate_frontmatter.py` | 验证文章 frontmatter |
-| `python3 scripts/check_links.py` | 检查内部链接有效性 |
+| `python3 scripts/check_links.py` | 检查 Markdown 与组件内部链接有效性 |
| `python3 scripts/check_quality.py documents/` | 内容质量检查 |
| `python3 scripts/build_examples.py --host` | 编译主机侧 CMake 示例 |
| `python3 scripts/build_examples.py --stm32` | 编译 STM32 示例工程 |
diff --git a/documents/en/vol1-fundamentals/ch03/index.md b/documents/en/vol1-fundamentals/ch03/index.md
index 65c1c6a27..53513dac8 100644
--- a/documents/en/vol1-fundamentals/ch03/index.md
+++ b/documents/en/vol1-fundamentals/ch03/index.md
@@ -1,15 +1,16 @@
---
-title: Arrays and Strings
-description: The evolution from C-style arrays to std::array and std::string
+title: Function
+description: Function definition, parameter passing, overloading, and constexpr functions
---
-# Arrays and Strings
+# Functions
-Data needs to live somewhere, and arrays and strings are the most fundamental containers. In this chapter, we first review the underlying mechanics of C-style arrays—understanding exactly why they are so "bare"—and then jump straight to `std::array` to experience how sweet zero-overhead abstraction can be. The string section follows a similar path, transitioning from C-style strings to `std::string`. You will find that handling text in modern C++ is leagues ahead of C.
+Functions are the fundamental units of code organization, and they represent our first step from "writing scripts" to "writing engineering code." In this chapter, we start with function definition and invocation, focusing on the different parameter passing mechanisms—by value, by reference, and by pointer. Understanding the differences between them is a prerequisite for writing efficient code. We then look at how function overloading and default parameters work together, and finally, we explore `inline` and `constexpr` functions, two keywords that appear frequently in embedded and performance-sensitive scenarios.
## Chapter Contents
- C-style Arrays
- std::array
- std::string
+ Function Basics
+ Parameter Passing Mechanisms
+ Overloading and Default Parameters
+ inline and constexpr Functions
diff --git a/documents/en/vol1-fundamentals/ch05/index.md b/documents/en/vol1-fundamentals/ch05/index.md
index 53513dac8..65c1c6a27 100644
--- a/documents/en/vol1-fundamentals/ch05/index.md
+++ b/documents/en/vol1-fundamentals/ch05/index.md
@@ -1,16 +1,15 @@
---
-title: Function
-description: Function definition, parameter passing, overloading, and constexpr functions
+title: Arrays and Strings
+description: The evolution from C-style arrays to std::array and std::string
---
-# Functions
+# Arrays and Strings
-Functions are the fundamental units of code organization, and they represent our first step from "writing scripts" to "writing engineering code." In this chapter, we start with function definition and invocation, focusing on the different parameter passing mechanisms—by value, by reference, and by pointer. Understanding the differences between them is a prerequisite for writing efficient code. We then look at how function overloading and default parameters work together, and finally, we explore `inline` and `constexpr` functions, two keywords that appear frequently in embedded and performance-sensitive scenarios.
+Data needs to live somewhere, and arrays and strings are the most fundamental containers. In this chapter, we first review the underlying mechanics of C-style arrays—understanding exactly why they are so "bare"—and then jump straight to `std::array` to experience how sweet zero-overhead abstraction can be. The string section follows a similar path, transitioning from C-style strings to `std::string`. You will find that handling text in modern C++ is leagues ahead of C.
## Chapter Contents
- Function Basics
- Parameter Passing Mechanisms
- Overloading and Default Parameters
- inline and constexpr Functions
+ C-style Arrays
+ std::array
+ std::string
diff --git a/documents/vol1-fundamentals/ch11/index.md b/documents/vol1-fundamentals/ch11/index.md
index af36ed4cb..88bad49c9 100644
--- a/documents/vol1-fundamentals/ch11/index.md
+++ b/documents/vol1-fundamentals/ch11/index.md
@@ -1,13 +1,14 @@
---
-title: "内存模型基础"
+title: "STL 初见"
---
-# 内存模型基础
+# STL 初见
## 本章内容
- 内存布局
- 动态内存管理
- 内存对齐与填充
+ std::vector 快速上手
+ 关联容器快速上手
+ 算法库初见
+ STL 常用模式
diff --git a/documents/vol1-fundamentals/ch12/index.md b/documents/vol1-fundamentals/ch12/index.md
index 88bad49c9..af36ed4cb 100644
--- a/documents/vol1-fundamentals/ch12/index.md
+++ b/documents/vol1-fundamentals/ch12/index.md
@@ -1,14 +1,13 @@
---
-title: "STL 初见"
+title: "内存模型基础"
---
-# STL 初见
+# 内存模型基础
## 本章内容
- std::vector 快速上手
- 关联容器快速上手
- 算法库初见
- STL 常用模式
+ 内存布局
+ 动态内存管理
+ 内存对齐与填充
diff --git a/package.json b/package.json
index fce6cbfa6..f0a64e72c 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,9 @@
"type": "module",
"scripts": {
"dev": "vitepress dev site",
- "build": "tsx scripts/build.ts",
- "build:single": "vitepress build site",
+ "check:links": "python3 scripts/check_links.py",
+ "build": "pnpm check:links && tsx scripts/build.ts",
+ "build:single": "pnpm check:links && vitepress build site",
"preview": "vitepress preview site",
"hooks:install": "scripts/setup_precommit.sh",
"coverage": "python3 scripts/coverage.py",
diff --git a/scripts/check_links.py b/scripts/check_links.py
index 47980ee62..1db75c5a1 100755
--- a/scripts/check_links.py
+++ b/scripts/check_links.py
@@ -25,8 +25,8 @@ class LinkChecker:
# Image extensions to check against filesystem
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.bmp', '.webp', '.ico'}
- # Files to skip from checking
- SKIP_FILES = {'index.md', 'tags.md'}
+ # Files to skip from checking
+ SKIP_FILES = {'tags.md'}
def __init__(self, tutorial_dir: Path, fix: bool = False):
self.tutorial_dir = tutorial_dir
@@ -56,8 +56,13 @@ def extract_links(self, content: str, filepath: Path) -> List[Tuple[int, str, st
Returns: List of (line_number, link_text, link_url)
"""
links = []
- # Match [text](url) and [text]()
- pattern = r'\[([^\]]+)\]\(([^)]+)\)|\[([^\]]+)\]\(<([^>]+)>\)'
+ # Match [text](url) and [text]()
+ markdown_pattern = r'\[([^\]]+)\]\(([^)]+)\)|\[([^\]]+)\]\(<([^>]+)>\)'
+ # Match Vue components with literal href attributes, such as
+ # Title.
+ component_href_pattern = (
+ r'<([A-Z][\w.]*)\b[^>]*\bhref=(["\'])([^"\']+)\2'
+ )
in_code_block = False
for line_num, line in enumerate(content.split('\n'), 1):
@@ -72,34 +77,52 @@ def extract_links(self, content: str, filepath: Path) -> List[Tuple[int, str, st
# Strip inline code spans to avoid matching C++ syntax like `[&](args)`
cleaned = re.sub(r'`[^`]+`', '', line)
- for match in re.finditer(pattern, cleaned):
- groups = match.groups()
- if groups[1]: # Regular link
- link_text, link_url = groups[0], groups[1]
- else: # Angle bracket link
- link_text, link_url = groups[2], groups[3]
-
- links.append((line_num, link_text, link_url))
-
- return links
-
- def normalize_path(self, link_url: str, source_file: Path) -> str:
- """Normalize a relative link path."""
- # Remove fragments/anchors
- link_url = link_url.split('#')[0]
- if not link_url:
- return ''
-
- # Get source directory
- source_dir = source_file.parent
-
- # Resolve relative path
- try:
- resolved = (source_dir / link_url).resolve()
- # Convert back to relative path from tutorial_dir
- return str(resolved.relative_to(self.tutorial_dir))
- except (ValueError, RuntimeError):
- return link_url
+ for match in re.finditer(markdown_pattern, cleaned):
+ groups = match.groups()
+ if groups[1]: # Regular link
+ link_text, link_url = groups[0], groups[1]
+ else: # Angle bracket link
+ link_text, link_url = groups[2], groups[3]
+
+ links.append((line_num, link_text, link_url))
+
+ for match in re.finditer(component_href_pattern, cleaned):
+ component_name = match.group(1)
+ link_url = match.group(3)
+ links.append((line_num, component_name, link_url))
+
+ return links
+
+ def candidate_paths(self, link_url: str, source_file: Path) -> List[str]:
+ """Return possible markdown targets for a link path."""
+ # Remove fragments/anchors
+ link_url = link_url.split('#')[0]
+ if not link_url:
+ return []
+
+ # VitePress treats a trailing slash as an index page.
+ link_url = link_url.rstrip('/')
+
+ if not link_url:
+ link_url = 'index'
+
+ if link_url.startswith('/'):
+ target = self.tutorial_dir / link_url.lstrip('/')
+ else:
+ target = source_file.parent / link_url
+
+ try:
+ resolved = target.resolve()
+ rel = resolved.relative_to(self.tutorial_dir)
+ except (ValueError, RuntimeError):
+ return [link_url]
+
+ candidates = [rel]
+ if rel.suffix != '.md':
+ candidates.append(rel.with_suffix('.md'))
+ candidates.append(rel / 'index.md')
+
+ return [str(candidate) for candidate in candidates]
def check_file(self, filepath: Path):
"""Check links in a single file."""
@@ -121,49 +144,32 @@ def check_file(self, filepath: Path):
continue
# Check image links against filesystem
- link_ext = Path(link_url.split('#')[0]).suffix.lower()
- if link_ext in self.IMAGE_EXTENSIONS:
- normalized = self.normalize_path(link_url, filepath)
- if normalized:
- resolved = self.tutorial_dir / normalized
- if not resolved.exists():
- self.errors.append(
- f"{rel_path}:{line_num} - Broken image: [{link_text}]({link_url})"
- )
- continue
-
- # Normalize the link path
- normalized = self.normalize_path(link_url, filepath)
-
- if not normalized:
- continue
-
- # Check if file exists
- if normalized not in self.all_files:
- # Try with .md extension if missing
- if not normalized.endswith('.md'):
- normalized_with_md = normalized + '.md'
- if normalized_with_md not in self.all_files:
- self.errors.append(
- f"{rel_path}:{line_num} - Broken link: [{link_text}]({link_url})"
- )
- else:
- # Track for potential fix
- self.warnings.append(
- f"{rel_path}:{line_num} - Missing .md extension: [{link_text}]({link_url})"
- )
- if self.fix:
- self.suggest_fix(filepath, line_num, link_url, link_url + '.md')
- else:
- self.errors.append(
- f"{rel_path}:{line_num} - Broken link: [{link_text}]({link_url})"
- )
- else:
- # Track valid link for reverse index
- key = str(normalized)
- if key not in self.link_map:
- self.link_map[key] = []
- self.link_map[key].append((filepath, link_text))
+ link_ext = Path(link_url.split('#')[0]).suffix.lower()
+ if link_ext in self.IMAGE_EXTENSIONS:
+ candidates = self.candidate_paths(link_url, filepath)
+ if candidates and not any((self.tutorial_dir / candidate).exists() for candidate in candidates):
+ self.errors.append(
+ f"{rel_path}:{line_num} - Broken image: [{link_text}]({link_url})"
+ )
+ continue
+
+ candidates = self.candidate_paths(link_url, filepath)
+
+ if not candidates:
+ continue
+
+ # Check if file exists
+ existing = next((candidate for candidate in candidates if candidate in self.all_files), None)
+ if existing is None:
+ self.errors.append(
+ f"{rel_path}:{line_num} - Broken link: [{link_text}]({link_url})"
+ )
+ else:
+ # Track valid link for reverse index
+ key = str(existing)
+ if key not in self.link_map:
+ self.link_map[key] = []
+ self.link_map[key].append((filepath, link_text))
def suggest_fix(self, filepath: Path, line_num: int, old_link: str, new_link: str):
"""Suggest a fix for a link."""