Skip to content

Commit 5b40f12

Browse files
feat: 重构了一些教程
- WSL / markdown / git 七篇按 writting_style.md 重写叙事框架(技术内容保留) - 侧栏按 frontmatter order 排学习序(不再字母序) - 接入 mermaid(markdown-it 拦截 + CDN 客户端渲染,参考 ModernCPP) - 首页 feature 卡片加 link,可点击直达各门类
1 parent 01f5e95 commit 5b40f12

46 files changed

Lines changed: 2803 additions & 3042 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,12 @@
77
88
🌐 **[在线文档站](https://awesome-embedded-learning-studio.github.io/EmbedBox/)**
99

10-
作者 Charliechen114514 · 联系方式 725610365@qq.com
11-
12-
本项目隶属于组织 [Awesome-Embedded-Learning-Studio](https://github.com/Awesome-Embedded-Learning-Studio) 的文档教程体系。
13-
1410
## 这是什么?
1511

1612
EmbedBox 是面向嵌入式开发者的「公共语言」入门坡道:在进入 AwesomeQt / ModernCPP / PenguinLAB / 各 forge 之前,先把终端、Git、Markdown、编译链、CMake、GDB、交叉编译、串口这些通用工具学会。它只负责把工具教透,不替中心站 [Awesome-Embedded](https://github.com/Awesome-Embedded-Learning-Studio/Awesome-Embedded) 做导航。
1713

1814
**适合谁?** 刚起步的嵌入式自学者 · 只会一点 PowerShell / 命令行的硬件朋友 · 想补齐「工具链前置知识」再啃具体航线的开发者。
1915

20-
## 快速目录
21-
22-
- [WSL2 + Linux + VS Code 远程开发](./tutorial/WSL/wsl.md)
23-
- [Markdown 零基础入门](./tutorial/markdown/markdown.md)
24-
- [Git 团队协作完全入门指南](./tutorial/git/index.md)(分 7 篇)
25-
2616
## 本地预览 / 构建
2717

2818
站点引擎为 [VitePress](https://vitepress.dev),需要 Node 22 + pnpm 10:
@@ -43,8 +33,6 @@ pnpm preview # 预览构建产物
4333
| [`examples/`](./examples/instructions.md) | 配套示例代码 / 硬件电路图 / PCB 等资产 |
4434
| [`project.config.ts`](./project.config.ts) | 站点元信息(名称、导航、侧栏 volume 等) |
4535

46-
推送到 `main` 分支会自动经 GitHub Actions 构建并部署到 GitHub Pages;提 PR 时 `Build Check` 会自动跑 `pnpm build` 校验(含死链检查)。
47-
4836
## 许可证与联系方式
4937

5038
- **许可证**[MIT License](./LICENSE)

project.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default defineProject({
4848
cppTemplateEscape: false,
4949
kbd: false,
5050
math: false,
51+
mermaid: true,
5152
},
5253

5354
favicon: '/EmbedBox/Awesome-Embedded.ico',

site/.vitepress/config/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface ProjectConfig {
5252
cppTemplateEscape?: boolean
5353
kbd?: boolean
5454
math?: boolean
55+
mermaid?: boolean
5556
}
5657

5758
homeBanner?: Record<string, string>

site/.vitepress/config/sidebar.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,26 @@ function humanize(name: string): string {
2323
.replace(/\b\w/g, c => c.toUpperCase())
2424
}
2525

26-
function sortEntries(a: string, b: string): number {
27-
const na = a.match(/^(\d+)/)?.[1]
28-
const nb = b.match(/^(\d+)/)?.[1]
29-
if (na && nb) return parseInt(na) - parseInt(nb)
30-
if (na) return -1
31-
if (nb) return 1
32-
return a.localeCompare(b, 'en')
26+
function extractOrder(filePath: string): number | null {
27+
try {
28+
const content = readFileSync(filePath, 'utf-8')
29+
const m = content.match(/^order:\s*(-?\d+)/m)
30+
return m ? parseInt(m[1], 10) : null
31+
} catch { /* ignore */ }
32+
return null
33+
}
34+
35+
// 排序键:优先读 frontmatter 的 order(目录读其 index.md),
36+
// 其次按文件名数字前缀,最后置末尾按名字排——让门类/章节按学习顺序而非字母序排列。
37+
function entryOrder(fullPath: string): number {
38+
let fmPath = fullPath
39+
try {
40+
if (statSync(fullPath).isDirectory()) fmPath = join(fullPath, 'index.md')
41+
} catch { /* ignore */ }
42+
const o = extractOrder(fmPath)
43+
if (o != null) return o
44+
const np = fullPath.split(/[/\\]/).pop()!.match(/^(\d+)/)?.[1]
45+
return np ? parseInt(np, 10) : Number.MAX_SAFE_INTEGER
3346
}
3447

3548
function scanDir(dir: string, urlPrefix: string, depth = 0): SidebarItem[] {
@@ -46,7 +59,12 @@ function scanDir(dir: string, urlPrefix: string, depth = 0): SidebarItem[] {
4659
)
4760
} catch { return [] }
4861

49-
entries.sort(sortEntries)
62+
entries.sort((a, b) => {
63+
const oa = entryOrder(join(dir, a))
64+
const ob = entryOrder(join(dir, b))
65+
if (oa !== ob) return oa - ob
66+
return a.localeCompare(b, 'en')
67+
})
5068
const items: SidebarItem[] = []
5169

5270
for (const name of entries) {

site/.vitepress/plugins/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ export function resolvePlugins(md: MarkdownIt, config: ProjectConfig): void {
1010
const { kbdPlugin } = require('./kbd-plugin')
1111
md.use(kbdPlugin)
1212
}
13+
if (config.plugins.mermaid) {
14+
const { mermaidPlugin } = require('./mermaid-plugin')
15+
md.use(mermaidPlugin)
16+
}
1317
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { PluginSimple } from 'markdown-it'
2+
import type MarkdownIt from 'markdown-it'
3+
4+
export const mermaidPlugin: PluginSimple = (md: MarkdownIt) => {
5+
// 把 mermaid 围栏在 Shiki 之前改写成自定义 token,避免 "language not loaded" 警告。
6+
// core 规则在分词之后、渲染之前执行,所以无论 VitePress 何时覆盖 Shiki 的 fence 渲染都生效。
7+
md.core.ruler.push('mermaid_block', (state) => {
8+
for (let i = 0; i < state.tokens.length; i++) {
9+
const token = state.tokens[i]
10+
if (token.type === 'fence' && token.info.trim() === 'mermaid') {
11+
token.type = 'mermaid_diagram'
12+
token.tag = ''
13+
token.nesting = 0
14+
}
15+
}
16+
return true
17+
})
18+
19+
md.renderer.rules.mermaid_diagram = (tokens, idx) => {
20+
const encoded = encodeURIComponent(tokens[idx].content.trim())
21+
return `<div class="mermaid-diagram" data-mermaid="${encoded}" data-rendered="false"></div>`
22+
}
23+
}

site/.vitepress/theme/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Theme } from 'vitepress'
44
import HomeTipBanner from './components/HomeTipBanner.vue'
55
import ChapterNav from './components/ChapterNav.vue'
66
import ChapterLink from './components/ChapterLink.vue'
7+
import { setupMermaid } from './mermaid-client'
78
import './custom.css'
89

910
export default {
@@ -16,5 +17,8 @@ export default {
1617
enhanceApp({ app }) {
1718
app.component('ChapterNav', ChapterNav)
1819
app.component('ChapterLink', ChapterLink)
20+
},
21+
setup() {
22+
setupMermaid()
1923
}
2024
} satisfies Theme
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { nextTick, onMounted } from 'vue'
2+
import { useRouter } from 'vitepress'
3+
4+
declare global {
5+
interface Window {
6+
mermaid?: {
7+
initialize: (config: Record<string, unknown>) => void
8+
render: (id: string, text: string) => Promise<{ svg: string; bindFunctions?: (el: Element) => void }>
9+
}
10+
__mermaidLoadingPromise__?: Promise<void>
11+
__mermaidInitialized__?: boolean
12+
}
13+
}
14+
15+
const MERMAID_CDN = 'https://cdn.jsdelivr.net/npm/mermaid@10.9.6/dist/mermaid.min.js'
16+
17+
function loadMermaid(): Promise<void> {
18+
if (typeof window === 'undefined') return Promise.resolve()
19+
20+
if (window.mermaid) {
21+
initMermaid()
22+
return Promise.resolve()
23+
}
24+
25+
if (window.__mermaidLoadingPromise__) return window.__mermaidLoadingPromise__
26+
27+
window.__mermaidLoadingPromise__ = new Promise<void>((resolve, reject) => {
28+
const existing = document.querySelector<HTMLScriptElement>('script[data-mermaid-runtime]')
29+
if (existing) {
30+
existing.addEventListener('load', () => { initMermaid(); resolve() })
31+
existing.addEventListener('error', () => reject(new Error('Failed to load Mermaid')))
32+
return
33+
}
34+
35+
const script = document.createElement('script')
36+
script.src = MERMAID_CDN
37+
script.async = true
38+
script.dataset.mermaidRuntime = 'true'
39+
script.onload = () => { initMermaid(); resolve() }
40+
script.onerror = () => reject(new Error(`Failed to load Mermaid from ${MERMAID_CDN}`))
41+
document.head.appendChild(script)
42+
})
43+
44+
return window.__mermaidLoadingPromise__
45+
}
46+
47+
function initMermaid() {
48+
if (!window.mermaid || window.__mermaidInitialized__) return
49+
window.mermaid.initialize({
50+
startOnLoad: false,
51+
securityLevel: 'loose',
52+
theme: 'default',
53+
flowchart: {
54+
htmlLabels: true,
55+
nodeSpacing: 50,
56+
rankSpacing: 50,
57+
padding: 15,
58+
},
59+
themeVariables: {
60+
fontSize: '15px',
61+
},
62+
})
63+
window.__mermaidInitialized__ = true
64+
}
65+
66+
async function renderMermaidDiagrams() {
67+
if (typeof window === 'undefined') return
68+
69+
await loadMermaid()
70+
await nextTick()
71+
await new Promise<void>((r) => requestAnimationFrame(() => r()))
72+
73+
const mermaid = window.mermaid
74+
if (!mermaid) return
75+
76+
const nodes = Array.from(
77+
document.querySelectorAll<HTMLElement>('.mermaid-diagram[data-rendered="false"]')
78+
)
79+
80+
for (let i = 0; i < nodes.length; i++) {
81+
const el = nodes[i]
82+
const raw = el.dataset.mermaid
83+
if (!raw) continue
84+
85+
const source = decodeURIComponent(raw)
86+
const id = `mermaid-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}`
87+
88+
try {
89+
const { svg } = await mermaid.render(id, source)
90+
el.innerHTML = svg
91+
el.dataset.rendered = 'true'
92+
} catch {
93+
el.dataset.rendered = 'error'
94+
el.innerHTML = `<pre class="mermaid-error">${escapeHtml(source)}</pre>`
95+
}
96+
}
97+
}
98+
99+
function escapeHtml(s: string) {
100+
return s.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
101+
.replaceAll('"', '&quot;').replaceAll("'", '&#39;')
102+
}
103+
104+
export function setupMermaid() {
105+
const router = useRouter()
106+
107+
onMounted(() => renderMermaidDiagrams())
108+
router.onAfterRouteChange = () => renderMermaidDiagrams()
109+
}

0 commit comments

Comments
 (0)