Skip to content

Commit e8b2a09

Browse files
committed
fix: stabilize site rendering and tooling
1 parent 8d421cf commit e8b2a09

12 files changed

Lines changed: 841 additions & 19 deletions

File tree

‎.github/workflows/pages.yml‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,26 @@ jobs:
2020
- name: Check out repository
2121
uses: actions/checkout@v6
2222

23-
- name: Set up Python
24-
uses: actions/setup-python@v6
23+
- name: Set up uv
24+
uses: astral-sh/setup-uv@v8
2525
with:
2626
python-version: "3.13"
27-
cache: pip
27+
enable-cache: true
2828

2929
- name: Install site dependencies
30-
run: python -m pip install -r requirements.txt
30+
run: uv sync --locked
3131

3232
- name: Test content validator
33-
run: python -m unittest discover -s tests -v
33+
run: uv run --locked python -m unittest discover -s tests -v
3434

3535
- name: Test interactive lab models
36-
run: node --test tests/labs/*.test.mjs
36+
run: node --test tests/**/*.test.mjs
3737

3838
- name: Validate public course
39-
run: python scripts/validate_guide.py .
39+
run: uv run --locked python scripts/validate_guide.py .
4040

4141
- name: Build static site
42-
run: mkdocs build --strict
42+
run: uv run --locked mkdocs build --strict
4343

4444
- name: Configure GitHub Pages
4545
uses: actions/configure-pages@v5

‎README.md‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,17 @@ cd analog-electronics-guide
4141
## 本地预览与验证
4242

4343
```bash
44-
python3 -m venv .venv
45-
.venv/bin/pip install -r requirements.txt
46-
python3 -m unittest discover -s tests -v
47-
node --test tests/labs/*.test.mjs
48-
python3 scripts/validate_guide.py .
49-
.venv/bin/mkdocs serve
44+
uv sync
45+
uv run python -m unittest discover -s tests -v
46+
node --test tests/**/*.test.mjs
47+
uv run python scripts/validate_guide.py .
48+
uv run mkdocs serve
5049
```
5150

5251
生产构建使用:
5352

5453
```bash
55-
.venv/bin/mkdocs build --strict
54+
uv run mkdocs build --strict
5655
```
5756

5857
## 许可证

‎docs/assets/javascripts/mathjax.js‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,43 @@ window.MathJax = {
55
processEscapes: true,
66
processEnvironments: true,
77
},
8+
startup: {
9+
typeset: false,
10+
},
811
options: {
912
ignoreHtmlClass: ".*|",
1013
processHtmlClass: "arithmatex",
1114
},
1215
};
1316

17+
function prepareTableOfContentsMath(navigation) {
18+
const formulaPattern = /\\\([\s\S]*?\\\)|\\\[[\s\S]*?\\\]/g;
19+
20+
navigation.querySelectorAll(".md-ellipsis").forEach((label) => {
21+
const source = label.textContent;
22+
const fragment = document.createDocumentFragment();
23+
let cursor = 0;
24+
let match;
25+
26+
while ((match = formulaPattern.exec(source)) !== null) {
27+
fragment.append(document.createTextNode(source.slice(cursor, match.index)));
28+
const formula = document.createElement("span");
29+
formula.className = "arithmatex";
30+
formula.textContent = match[0];
31+
fragment.append(formula);
32+
cursor = match.index + match[0].length;
33+
}
34+
35+
if (cursor > 0) {
36+
fragment.append(document.createTextNode(source.slice(cursor)));
37+
label.replaceChildren(fragment);
38+
}
39+
});
40+
}
41+
1442
document$.subscribe(() => {
15-
MathJax.typesetPromise();
43+
document
44+
.querySelectorAll(".md-nav--secondary")
45+
.forEach(prepareTableOfContentsMath);
46+
return MathJax.typesetPromise();
1647
});

‎docs/assets/stylesheets/extra.css‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,37 @@ body {
388388
width: 100%;
389389
}
390390

391+
.md-typeset svg.ae-figure .bg {
392+
fill: var(--ae-paper-raised);
393+
}
394+
395+
.md-typeset svg.ae-figure .ink,
396+
.md-typeset svg.ae-figure .axis {
397+
stroke: var(--ae-ink);
398+
}
399+
400+
.md-typeset svg.ae-figure .node {
401+
fill: var(--ae-ink);
402+
}
403+
404+
.md-typeset svg.ae-figure .dot {
405+
stroke: var(--ae-ink);
406+
}
407+
408+
.md-typeset svg.ae-figure .txt,
409+
.md-typeset svg.ae-figure .head,
410+
.md-typeset svg.ae-figure .math {
411+
fill: var(--ae-ink);
412+
}
413+
414+
.md-typeset svg.ae-figure .small {
415+
fill: var(--ae-muted);
416+
}
417+
418+
.md-typeset svg.ae-figure .panel {
419+
stroke: color-mix(in srgb, var(--ae-muted) 72%, transparent);
420+
}
421+
391422
.md-typeset .ae-figure-frame figcaption {
392423
color: var(--ae-muted);
393424
font-family: "Avenir Next Condensed", "DIN Condensed", sans-serif;

‎docs/guide/03-BJT与MOSFET.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,7 +1431,7 @@ g_{m,\mathrm{MOS}}=\frac{2(1.00\ \mathrm{mA})}{0.20\ \mathrm V}
14311431

14321432
| 高频问题 | 30 秒回答 | 90 秒回答扩展 | 适用边界与递进追问 |
14331433
|---|---|---|---|
1434-
| 为什么 BJT 能放大? | 先用直流偏置建立正向有源 Q 点,再让小 \(v_{be}\) 通过 \(g_m=I_{CQ}/V_T\) 产生较大的可预测 \(i_c\) 增量;能量来自直流电源,不来自输入信号本身。 | 补发射极注入—薄基区扩散—BC 结收集的物理链,再从指数式在 Q 点求导;说明无偏置或信号过大会跨截止/饱和并失真。 | 边界:正向有源、低频、\(|\tilde v_{be}|\ll V_T\)。追问一:输出能量从哪里来?追问二:为什么 Q 点太靠近截止会失真? |
1434+
| 为什么 BJT 能放大? | 先用直流偏置建立正向有源 Q 点,再让小 \(v_{be}\) 通过 \(g_m=I_{CQ}/V_T\) 产生较大的可预测 \(i_c\) 增量;能量来自直流电源,不来自输入信号本身。 | 补发射极注入—薄基区扩散—BC 结收集的物理链,再从指数式在 Q 点求导;说明无偏置或信号过大会跨截止/饱和并失真。 | 边界:正向有源、低频、\(\lvert\tilde v_{be}\rvert\ll V_T\)。追问一:输出能量从哪里来?追问二:为什么 Q 点太靠近截止会失真? |
14351435
| \(\beta\) 是什么,为什么不可靠? | 正向有源时 \(\beta=I_C/I_B\),且 \(\beta=\alpha/(1-\alpha)\);它随个体、偏置、温度和 \(V_{CE}\) 变,不能当精密常数。 | 从 \(I_E=I_B+I_C\) 推出与 \(\alpha\) 的关系;强调 \(\beta I_B\) 是端口近似,饱和时集电流由外电路限制,应靠发射极反馈稳定偏置。 | 边界:只在给定正向有源工作点附近。追问一:\(\alpha\) 接近 1 时为何 \(\beta\) 很敏感?追问二:怎样降低偏置对 \(\beta\) 的依赖? |
14361436
| 怎样判断 BJT 工作区? | 假设区域、代模型、列 KCL/KVL、求解,再检查 \(V_{BE},V_{BC}\) 和电流方向;矛盾就换区。 | 截止是两结不正偏,正向有源是 BE 正偏/BC 反偏,饱和是两结都正偏;有源假设若给 \(V_{BC}>0\),立即停止使用 \(\beta I_B\)。 | 边界:结偏置过渡连续,分段压降由题设决定。追问一:为何只看 \(V_{CE}\) 不够根本?追问二:BJT 饱和与 MOS 饱和有何不同? |
14371437
| 为什么 MOS 栅电流约为零? | 栅与半导体之间有绝缘氧化层,理想稳态没有导电通路,所以 \(I_G\approx0\)。 | 栅电场仍能通过电容耦合改变表面电荷,先耗尽再反型;动态时有 \(i=C\,\mathrm dv/\mathrm dt\) 和栅电荷,实际还有漏电与氧化层击穿。 | 边界:只指理想低频直流。追问一:开关损耗为何仍与栅电荷有关?追问二:输入电阻能否在任意频率都视为无穷大? |

‎mkdocs.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ plugins:
100100
- zh
101101
- en
102102

103+
hooks:
104+
- scripts/mkdocs_hooks.py
105+
103106
extra_css:
104107
- assets/stylesheets/extra.css
105108

‎pyproject.toml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[project]
2+
name = "analog-electronics-guide"
3+
version = "0.1.0"
4+
description = "A 30-day analog electronics self-study guide"
5+
readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = [
8+
"mkdocs-material==9.7.7",
9+
]
10+
11+
[tool.uv]
12+
package = false

‎requirements.txt‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎scripts/mkdocs_hooks.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""MkDocs build hooks for browser-stable course rendering."""
2+
3+
from __future__ import annotations
4+
5+
import html as html_module
6+
import re
7+
from pathlib import Path
8+
from urllib.parse import unquote, urlsplit
9+
10+
11+
TEACHING_FIGURE_RE = re.compile(
12+
r'<img\b'
13+
r'(?=[^>]*\bclass="[^"]*\bae-figure\b[^"]*")'
14+
r'(?=[^>]*\bsrc="(?P<src>[^"]+\.svg)")'
15+
r"[^>]*/?>"
16+
)
17+
SVG_OPEN_RE = re.compile(r"<svg\b(?P<attributes>[^>]*)>", re.IGNORECASE)
18+
CLASS_RE = re.compile(r'\bclass="(?P<classes>[^"]*)"', re.IGNORECASE)
19+
ID_RE = re.compile(r'\bid="(?P<id>[^"]+)"')
20+
21+
22+
def _prefix_svg_ids(svg: str, prefix: str) -> str:
23+
"""Keep marker and gradient IDs unique after several SVGs are inlined."""
24+
25+
identifiers = ID_RE.findall(svg)
26+
for identifier in identifiers:
27+
replacement = f"{prefix}{identifier}"
28+
svg = svg.replace(f'id="{identifier}"', f'id="{replacement}"')
29+
svg = svg.replace(f"url(#{identifier})", f"url(#{replacement})")
30+
svg = svg.replace(f'href="#{identifier}"', f'href="#{replacement}"')
31+
svg = svg.replace(f"xlink:href=\"#{identifier}\"", f'xlink:href="#{replacement}"')
32+
return svg
33+
34+
35+
def _add_figure_class(svg: str) -> str:
36+
def replace(match: re.Match[str]) -> str:
37+
attributes = match.group("attributes")
38+
class_match = CLASS_RE.search(attributes)
39+
if class_match:
40+
classes = class_match.group("classes").split()
41+
if "ae-figure" not in classes:
42+
classes.append("ae-figure")
43+
attributes = CLASS_RE.sub(
44+
f'class="{" ".join(classes)}"',
45+
attributes,
46+
count=1,
47+
)
48+
else:
49+
attributes = f' class="ae-figure"{attributes}'
50+
return f"<svg{attributes}>"
51+
52+
return SVG_OPEN_RE.sub(replace, svg, count=1)
53+
54+
55+
def on_page_content(html: str, page, config, files) -> str:
56+
"""Inline teaching SVGs so the selected site palette controls their colors."""
57+
58+
figure_dir = (Path(config.docs_dir) / "assets" / "figures").resolve()
59+
60+
def replace(match: re.Match[str]) -> str:
61+
source = html_module.unescape(match.group("src"))
62+
source_path = unquote(urlsplit(source).path)
63+
filename = Path(source_path).name
64+
figure_path = (figure_dir / filename).resolve()
65+
66+
if figure_path.parent != figure_dir or not figure_path.is_file():
67+
return match.group(0)
68+
69+
svg = figure_path.read_text(encoding="utf-8")
70+
prefix = f"ae-{figure_path.stem}-"
71+
return _add_figure_class(_prefix_svg_ids(svg, prefix))
72+
73+
return TEACHING_FIGURE_RE.sub(replace, html)

‎tests/runtime/mathjax.test.mjs‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import assert from "node:assert/strict";
2+
import { readFileSync } from "node:fs";
3+
import { runInNewContext } from "node:vm";
4+
import test from "node:test";
5+
6+
const script = readFileSync(
7+
new URL("../../docs/assets/javascripts/mathjax.js", import.meta.url),
8+
"utf8",
9+
);
10+
11+
function loadMathJaxConfig() {
12+
const subscribers = [];
13+
const labels = [
14+
{
15+
children: [],
16+
textContent: "9.1 从平方律推出 \\(g_m\\)",
17+
replaceChildren(fragment) {
18+
this.children = fragment.children;
19+
},
20+
},
21+
];
22+
const navigations = [
23+
{},
24+
{},
25+
];
26+
const context = {
27+
document$: {
28+
subscribe(callback) {
29+
subscribers.push(callback);
30+
},
31+
},
32+
document: {
33+
querySelectorAll(selector) {
34+
assert.equal(selector, ".md-nav--secondary");
35+
return navigations.map(() => ({
36+
querySelectorAll(innerSelector) {
37+
assert.equal(innerSelector, ".md-ellipsis");
38+
return labels;
39+
},
40+
}));
41+
},
42+
createDocumentFragment() {
43+
return {
44+
children: [],
45+
append(...nodes) {
46+
this.children.push(...nodes);
47+
},
48+
};
49+
},
50+
createElement(tagName) {
51+
return { className: "", tagName, textContent: "" };
52+
},
53+
createTextNode(textContent) {
54+
return { nodeType: 3, textContent };
55+
},
56+
},
57+
};
58+
context.window = context;
59+
runInNewContext(script, context);
60+
return { context, labels, navigations, subscribers };
61+
}
62+
63+
test("one document event produces exactly one MathJax typeset pass", async () => {
64+
const { context, subscribers } = loadMathJaxConfig();
65+
let typesetCalls = 0;
66+
context.MathJax.typesetPromise = async () => {
67+
typesetCalls += 1;
68+
};
69+
70+
assert.equal(context.MathJax.startup.typeset, false);
71+
assert.equal(subscribers.length, 1);
72+
await subscribers[0]();
73+
assert.equal(typesetCalls, 1);
74+
});
75+
76+
test("table-of-contents TeX is wrapped in a dedicated MathJax node", async () => {
77+
const { context, labels, subscribers } = loadMathJaxConfig();
78+
context.MathJax.typesetPromise = async () => {};
79+
80+
await subscribers[0]();
81+
82+
const formula = labels[0].children.find(
83+
(child) => child.className === "arithmatex",
84+
);
85+
assert.ok(formula);
86+
assert.equal(formula.textContent, "\\(g_m\\)");
87+
});

0 commit comments

Comments
 (0)