mirror of
https://github.com/riba2534/TCP-IP-NetworkNote.git
synced 2026-07-04 11:56:05 +08:00
feat: 新增 VitePress 电子书网站,部署到 Cloudflare Pages
将笔记转化为精美的 VitePress 静态电子书网站: - site/ 工程目录:构建脚本从 chXX/README.md + .c 源码 + images/ 幂等生成 19 个章节页 + 96 个源码页(每个 .c 独立页面,Shiki 语法高亮) - 构建脚本零依赖,处理 3 种代码链接形态(同章/跨章/绝对URL)+ 110 处图片路径转换,保持原 Markdown 结构不变 - 首页 hero 用 AI 生成的网络主题封面图,配套 favicon 多尺寸 - 中文衬线正文排版 + GitHub 风格代码主题 + 本地全文搜索 - GitHub Actions + wrangler 自动部署到 Cloudflare Pages - 域名 tcp.riba2534.cn 原 chXX/ 目录与根 README 保持不动,网站内容每次构建从源重新生成。
This commit is contained in:
37
.github/workflows/deploy.yml
vendored
Normal file
37
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Deploy to Cloudflare Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
deployments: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: site/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: site
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: npm run build
|
||||||
|
working-directory: site
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
|
command: pages deploy .vitepress/dist --project-name=tcp-ip-notes
|
||||||
|
workingDirectory: site
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 网站构建
|
||||||
|
site/node_modules/
|
||||||
|
site/.vitepress/dist/
|
||||||
|
site/.vitepress/cache/
|
||||||
|
|
||||||
|
# 脚本生成的章节内容(每次构建重新生成)
|
||||||
|
site/ch[0-9][0-9]/
|
||||||
|
|
||||||
|
# 脚本复制的图片
|
||||||
|
site/public/images/
|
||||||
25
site/.vitepress/chapters.mjs
Normal file
25
site/.vitepress/chapters.mjs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// 19 章数据 — config.mts 与 scripts/build.mjs 共享的单一数据源
|
||||||
|
// 新增/调整章节只改这里一处
|
||||||
|
// 用 .mjs(纯 JS)以便 Node 脚本和 VitePress 配置都能直接 import
|
||||||
|
|
||||||
|
export const CHAPTERS = [
|
||||||
|
{ dir: 'ch01', title: '第 1 章 理解网络编程和套接字' },
|
||||||
|
{ dir: 'ch02', title: '第 2 章 套接字类型与协议设置' },
|
||||||
|
{ dir: 'ch03', title: '第 3 章 地址族与数据序列' },
|
||||||
|
{ dir: 'ch04', title: '第 4 章 基于 TCP 的服务端/客户端(1)' },
|
||||||
|
{ dir: 'ch05', title: '第 5 章 基于 TCP 的服务端/客户端(2)' },
|
||||||
|
{ dir: 'ch06', title: '第 6 章 基于 UDP 的服务端/客户端' },
|
||||||
|
{ dir: 'ch07', title: '第 7 章 优雅地断开套接字的连接' },
|
||||||
|
{ dir: 'ch08', title: '第 8 章 域名及网络地址' },
|
||||||
|
{ dir: 'ch09', title: '第 9 章 套接字的多种可选项' },
|
||||||
|
{ dir: 'ch10', title: '第 10 章 多进程服务器端' },
|
||||||
|
{ dir: 'ch11', title: '第 11 章 进程间通信' },
|
||||||
|
{ dir: 'ch12', title: '第 12 章 I/O 复用' },
|
||||||
|
{ dir: 'ch13', title: '第 13 章 多种 I/O 函数' },
|
||||||
|
{ dir: 'ch14', title: '第 14 章 多播与广播' },
|
||||||
|
{ dir: 'ch15', title: '第 15 章 套接字和标准 I/O' },
|
||||||
|
{ dir: 'ch16', title: '第 16 章 关于 I/O 流分离的其他内容' },
|
||||||
|
{ dir: 'ch17', title: '第 17 章 优于 select 的 epoll' },
|
||||||
|
{ dir: 'ch18', title: '第 18 章 多线程服务器端的实现' },
|
||||||
|
{ dir: 'ch24', title: '第 24 章 制作 HTTP 服务器端' },
|
||||||
|
]
|
||||||
76
site/.vitepress/config.mts
Normal file
76
site/.vitepress/config.mts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
import { CHAPTERS } from './chapters.mjs'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
title: 'TCP/IP 网络编程学习笔记',
|
||||||
|
description: '《TCP/IP 网络编程》学习笔记电子书',
|
||||||
|
lang: 'zh-CN',
|
||||||
|
cleanUrls: true,
|
||||||
|
lastUpdated: true,
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
theme: { light: 'github-light', dark: 'github-dark' },
|
||||||
|
lineNumbers: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
head: [
|
||||||
|
['meta', { name: 'viewport', content: 'width=device-width,initial-scale=1' }],
|
||||||
|
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32.png' }],
|
||||||
|
['link', { rel: 'icon', type: 'image/png', sizes: '180x180', href: '/favicon-180.png' }],
|
||||||
|
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon-180.png' }],
|
||||||
|
['link', { rel: 'mask-icon', href: '/favicon-512.png', color: '#06b6d4' }],
|
||||||
|
['meta', { name: 'theme-color', content: '#0a0e27' }],
|
||||||
|
['meta', { property: 'og:image', content: '/cover.png' }],
|
||||||
|
],
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
siteTitle: 'TCP/IP 网络编程笔记',
|
||||||
|
outline: { level: [2, 3], label: '本页目录' },
|
||||||
|
docFooter: { prev: '上一章', next: '下一章' },
|
||||||
|
lastUpdatedText: '最后更新',
|
||||||
|
returnToTopLabel: '回到顶部',
|
||||||
|
sidebarMenuLabel: '目录',
|
||||||
|
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
options: {
|
||||||
|
translations: {
|
||||||
|
button: { buttonText: '搜索', buttonAriaLabel: '搜索' },
|
||||||
|
modal: {
|
||||||
|
displayDetails: '显示详情',
|
||||||
|
resetButtonTitle: '清除查询',
|
||||||
|
backButtonTitle: '返回',
|
||||||
|
noResultsText: '无结果',
|
||||||
|
footer: {
|
||||||
|
selectText: '选择',
|
||||||
|
navigateText: '切换',
|
||||||
|
closeText: '关闭',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
nav: [
|
||||||
|
{ text: '首页', link: '/' },
|
||||||
|
{ text: 'GitHub 仓库', link: 'https://github.com/riba2534/TCP-IP-NetworkNote' },
|
||||||
|
],
|
||||||
|
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
text: '章节',
|
||||||
|
collapsed: false,
|
||||||
|
items: CHAPTERS.map((c) => ({ text: c.title, link: `/${c.dir}/` })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/riba2534/TCP-IP-NetworkNote' },
|
||||||
|
],
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
message: '基于 VitePress 构建,部署于 Cloudflare Pages',
|
||||||
|
copyright: 'Copyright © riba2534',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
65
site/.vitepress/theme/custom.css
Normal file
65
site/.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* TCP/IP 网络编程笔记 — 电子书排版 */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
--vp-font-family-mono: 'JetBrains Mono', 'Fira Code', Menlo, Consolas,
|
||||||
|
'Courier New', monospace;
|
||||||
|
--content-max-width: 880px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正文段落用中文衬线,提升长文阅读舒适度 */
|
||||||
|
.vp-doc p,
|
||||||
|
.vp-doc li {
|
||||||
|
font-family: 'Source Han Serif SC', 'Noto Serif CJK SC', 'Songti SC', STSong,
|
||||||
|
'STZhongsong', serif;
|
||||||
|
line-height: 1.85;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题与 UI 用无衬线,保持清晰 */
|
||||||
|
.vp-doc h1,
|
||||||
|
.vp-doc h2,
|
||||||
|
.vp-doc h3,
|
||||||
|
.vp-doc h4 {
|
||||||
|
font-family: var(--vp-font-family-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码块 */
|
||||||
|
.vp-doc pre {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc code {
|
||||||
|
font-family: var(--vp-font-family-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行内代码微调 */
|
||||||
|
.vp-doc :not(pre) > code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片居中带阴影圆角 */
|
||||||
|
.vp-doc img {
|
||||||
|
display: block;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格交替行底色 */
|
||||||
|
.vp-doc table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 源码页"返回本章"链接 */
|
||||||
|
.vp-doc .back-to-chapter {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-family: var(--vp-font-family-base);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
4
site/.vitepress/theme/index.ts
Normal file
4
site/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import './custom.css'
|
||||||
|
|
||||||
|
export default DefaultTheme
|
||||||
32
site/index.md
Normal file
32
site/index.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: TCP/IP 网络编程
|
||||||
|
text: 学习笔记电子书
|
||||||
|
tagline: 19 章笔记 · 96 个 C 源码 · 在线浏览与语法高亮
|
||||||
|
image:
|
||||||
|
src: /cover.png
|
||||||
|
alt: TCP/IP 网络编程
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: 开始阅读
|
||||||
|
link: /ch01/
|
||||||
|
- theme: alt
|
||||||
|
text: GitHub 仓库
|
||||||
|
link: https://github.com/riba2534/TCP-IP-NetworkNote
|
||||||
|
|
||||||
|
features:
|
||||||
|
- icon: 📖
|
||||||
|
title: 19 章完整笔记
|
||||||
|
details: 从套接字基础到 epoll、多线程、HTTP 服务器实现,覆盖 TCP/IP 网络编程核心概念
|
||||||
|
- icon: 💻
|
||||||
|
title: 96 个源码在线看
|
||||||
|
details: 每个 .c 文件独立页面,GitHub 风格语法高亮,含习题代码
|
||||||
|
- icon: 🔍
|
||||||
|
title: 全文搜索
|
||||||
|
details: 本地索引,离线可用,快速定位知识点
|
||||||
|
- icon: ✨
|
||||||
|
title: 优雅排版
|
||||||
|
details: 中文衬线正文、代码行号、响应式布局,阅读体验舒适
|
||||||
|
---
|
||||||
2550
site/package-lock.json
generated
Normal file
2550
site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
site/package.json
Normal file
14
site/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "tcp-ip-network-notes-site",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"gen": "node scripts/build.mjs",
|
||||||
|
"dev": "npm run gen && vitepress dev",
|
||||||
|
"build": "npm run gen && vitepress build",
|
||||||
|
"preview": "vitepress preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "^1.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
site/public/cover.png
Normal file
BIN
site/public/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
site/public/favicon-180.png
Normal file
BIN
site/public/favicon-180.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
site/public/favicon-192.png
Normal file
BIN
site/public/favicon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
site/public/favicon-32.png
Normal file
BIN
site/public/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
site/public/favicon-512.png
Normal file
BIN
site/public/favicon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
BIN
site/public/favicon.png
Normal file
BIN
site/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
199
site/scripts/build.mjs
Normal file
199
site/scripts/build.mjs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
// 构建脚本:从仓库源(chXX/README.md + chXX/**/*.c + images/)生成 VitePress 站点内容
|
||||||
|
// 幂等:每次先清理生成物再全量生成。零第三方依赖,纯 Node fs/path。
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { CHAPTERS } from '../.vitepress/chapters.mjs'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, '../..') // 仓库根
|
||||||
|
const SITE_ROOT = path.resolve(__dirname, '..') // site/
|
||||||
|
|
||||||
|
const GENERATED_DIRS = CHAPTERS.map((c) => path.join(SITE_ROOT, c.dir))
|
||||||
|
const PUBLIC_IMAGES = path.join(SITE_ROOT, 'public/images')
|
||||||
|
|
||||||
|
// ---------- 工具 ----------
|
||||||
|
function rmrf(p) {
|
||||||
|
fs.rmSync(p, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
function mkdirp(p) {
|
||||||
|
fs.mkdirSync(p, { recursive: true })
|
||||||
|
}
|
||||||
|
// 递归列出目录下所有 .c 文件,返回相对该目录的路径(如 "hello_server.c"、"homework/kehou4.c")
|
||||||
|
function listCFiles(chDir) {
|
||||||
|
const result = []
|
||||||
|
function walk(dir, relBase) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name)
|
||||||
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name
|
||||||
|
if (entry.isDirectory()) walk(full, rel)
|
||||||
|
else if (entry.name.endsWith('.c')) result.push(rel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(chDir, '')
|
||||||
|
result.sort()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤 A:清理生成物 ----------
|
||||||
|
function clean() {
|
||||||
|
for (const d of GENERATED_DIRS) rmrf(d)
|
||||||
|
rmrf(PUBLIC_IMAGES)
|
||||||
|
mkdirp(path.join(SITE_ROOT, 'public'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤 B:复制图片 ----------
|
||||||
|
function copyImages() {
|
||||||
|
const src = path.join(REPO_ROOT, 'images')
|
||||||
|
if (!fs.existsSync(src)) {
|
||||||
|
console.warn(`[warn] 未找到 images 目录: ${src}`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
fs.cpSync(src, PUBLIC_IMAGES, { recursive: true })
|
||||||
|
return fs.readdirSync(PUBLIC_IMAGES).length
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤 C:生成 .c 源码页 ----------
|
||||||
|
function genSourcePages() {
|
||||||
|
let count = 0
|
||||||
|
for (const ch of CHAPTERS) {
|
||||||
|
const chDir = path.join(REPO_ROOT, ch.dir)
|
||||||
|
if (!fs.existsSync(chDir)) {
|
||||||
|
console.warn(`[warn] 章节目录不存在: ${ch.dir}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const outDir = path.join(SITE_ROOT, ch.dir)
|
||||||
|
mkdirp(outDir)
|
||||||
|
const cFiles = listCFiles(chDir)
|
||||||
|
for (const rel of cFiles) {
|
||||||
|
const code = fs.readFileSync(path.join(chDir, rel), 'utf8')
|
||||||
|
const baseName = path.basename(rel) // hello_server.c
|
||||||
|
const outMd = path.join(outDir, `${rel}.md`) // .../hello_server.c.md 或 .../homework/kehou4.c.md
|
||||||
|
mkdirp(path.dirname(outMd))
|
||||||
|
// 相对返回链接:源码页可能在 homework/ 子目录,需回到章节根
|
||||||
|
const depth = rel.split('/').length - 1
|
||||||
|
const backHref = depth > 0 ? `${'../'.repeat(depth)}` : './'
|
||||||
|
const md = `---
|
||||||
|
title: ${baseName}
|
||||||
|
sidebar: false
|
||||||
|
---
|
||||||
|
|
||||||
|
<a class="back-to-chapter" href="${backHref}">← 返回${ch.title}</a>
|
||||||
|
|
||||||
|
# ${baseName}
|
||||||
|
|
||||||
|
\`\`\`c
|
||||||
|
${code}
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
fs.writeFileSync(outMd, md)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 链接重写 ----------
|
||||||
|
// 形态3:绝对 GitHub URL → 相对 clean URL(已验证均同章,但写稳健分支)
|
||||||
|
function rewriteGitHubLinks(md, curCh) {
|
||||||
|
const re =
|
||||||
|
/\]\(https:\/\/github\.com\/riba2534\/TCP-IP-NetworkNote\/blob\/master\/([^)]+)\)/g
|
||||||
|
return md.replace(re, (m, p) => {
|
||||||
|
// p 形如 "ch18/thread4.c" 或 "ch18/homework/x.c"
|
||||||
|
const parts = p.split('/')
|
||||||
|
const targetCh = parts[0]
|
||||||
|
const file = parts[parts.length - 1]
|
||||||
|
if (targetCh === curCh) {
|
||||||
|
// 同章:直接文件名(章节页在 chXX/index.md,同目录)
|
||||||
|
return `](${file})`
|
||||||
|
}
|
||||||
|
// 跨章:相对路径
|
||||||
|
return `](../${targetCh}/${file})`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 形态1/2:相对链接保持不变(镜像目录结构下天然正确)
|
||||||
|
// 但为避免 VitePress 对无扩展名链接报死链,给同章裸 .c 链接加 ./ 前缀
|
||||||
|
function normalizeRelativeLinks(md) {
|
||||||
|
// 匹配 ](xxx.c) 但不匹配已带 ./ 或 ../ 或 / 或 http 的
|
||||||
|
return md.replace(/\]\((?!\.\/|\.\.\/|\/|https?:|mailto:|#)([^)]+\.c)\)/g, (m, p) => `](./${p})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片路径:](images/ → ](/images/
|
||||||
|
function rewriteImages(md) {
|
||||||
|
return md.replaceAll('](images/', '](/images/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 步骤 D:转换章节 README → index.md ----------
|
||||||
|
function genChapterPages() {
|
||||||
|
let count = 0
|
||||||
|
for (let i = 0; i < CHAPTERS.length; i++) {
|
||||||
|
const ch = CHAPTERS[i]
|
||||||
|
const srcReadme = path.join(REPO_ROOT, ch.dir, 'README.md')
|
||||||
|
if (!fs.existsSync(srcReadme)) {
|
||||||
|
console.warn(`[warn] 章节笔记不存在: ${srcReadme}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let raw = fs.readFileSync(srcReadme, 'utf8')
|
||||||
|
|
||||||
|
// D1. 去掉首行 H1(## 第X章 …),由 frontmatter title 接管
|
||||||
|
raw = raw.replace(/^##\s+[^\n]*\n+/, '')
|
||||||
|
|
||||||
|
// D2. 图片路径转换
|
||||||
|
raw = rewriteImages(raw)
|
||||||
|
|
||||||
|
// D3. 代码链接重写
|
||||||
|
raw = rewriteGitHubLinks(raw, ch.dir)
|
||||||
|
raw = normalizeRelativeLinks(raw)
|
||||||
|
|
||||||
|
// D4. 追加"本章源码"附录
|
||||||
|
const chDir = path.join(REPO_ROOT, ch.dir)
|
||||||
|
if (fs.existsSync(chDir)) {
|
||||||
|
const cFiles = listCFiles(chDir)
|
||||||
|
if (cFiles.length > 0) {
|
||||||
|
raw += `\n\n## 本章源码\n\n`
|
||||||
|
for (const rel of cFiles) {
|
||||||
|
const baseName = path.basename(rel)
|
||||||
|
// 章节页在 chXX/index.md,源码页在 chXX/<rel>.md,相对链接加 ./ 前缀
|
||||||
|
raw += `- [${baseName}](./${rel})\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// D5. 注入 frontmatter(title + prev/next)
|
||||||
|
const prev =
|
||||||
|
i > 0
|
||||||
|
? { text: CHAPTERS[i - 1].title, link: `/${CHAPTERS[i - 1].dir}/` }
|
||||||
|
: { text: '首页', link: '/' }
|
||||||
|
const next =
|
||||||
|
i < CHAPTERS.length - 1
|
||||||
|
? { text: CHAPTERS[i + 1].title, link: `/${CHAPTERS[i + 1].dir}/` }
|
||||||
|
: null
|
||||||
|
|
||||||
|
const fm = [`---`, `title: ${ch.title}`, `prev:`, ` text: ${prev.text}`, ` link: ${prev.link}`]
|
||||||
|
if (next) {
|
||||||
|
fm.push(`next:`, ` text: ${next.text}`, ` link: ${next.link}`)
|
||||||
|
}
|
||||||
|
fm.push('---', '', `# ${ch.title}`, '')
|
||||||
|
|
||||||
|
const outDir = path.join(SITE_ROOT, ch.dir)
|
||||||
|
mkdirp(outDir)
|
||||||
|
fs.writeFileSync(path.join(outDir, 'index.md'), fm.join('\n') + raw)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 主流程 ----------
|
||||||
|
console.log('▶ 清理生成物...')
|
||||||
|
clean()
|
||||||
|
console.log('▶ 复制图片...')
|
||||||
|
const imgCount = copyImages()
|
||||||
|
console.log(` 图片: ${imgCount}`)
|
||||||
|
console.log('▶ 生成 .c 源码页...')
|
||||||
|
const srcCount = genSourcePages()
|
||||||
|
console.log(` 源码页: ${srcCount}`)
|
||||||
|
console.log('▶ 转换章节 README...')
|
||||||
|
const chCount = genChapterPages()
|
||||||
|
console.log(` 章节页: ${chCount}`)
|
||||||
|
console.log(`✓ 完成:${chCount} 章 + ${srcCount} 源码页 + ${imgCount} 图片`)
|
||||||
Reference in New Issue
Block a user