@chengzhao-dev/dsh-quarto-book 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,250 +1,250 @@
1
- #!/usr/bin/env python3
2
- """rendering-coords.py - 坐标轴网格图的共享 SVG 绘图助手。
3
-
4
- Quarto 章节里的 ``{python}`` 可执行单元调用本助手生成坐标轴网格图
5
- (棋盘行列、二维数组下标等),绘图逻辑只存在于本文件;正文单元应
6
- 只含调用,不内联第二套 SVG 逻辑(口径见 `quarto/coords.md`)。只用标准库拼 SVG;描边与文字用 currentColor,跟随
7
- 页面明暗主题,不写死颜色。
8
- """
9
-
10
- #===------------------------------------------------------------------------===#
11
- # 常量
12
- #===------------------------------------------------------------------------===#
13
-
14
- FONT_FAMILY = "LXGW Bright Medium, system-ui, sans-serif"
15
-
16
-
17
- #===------------------------------------------------------------------------===#
18
- # 绘图
19
- #===------------------------------------------------------------------------===#
20
-
21
- def render_grid(rows=3, cols=3, labels="coords", origin="top-left",
22
- cell=64, label_gap=30, font_size=14, marks=None,
23
- highlight=None, axis=True):
24
- """渲染一张程序坐标系网格图,返回 SVG 字符串。
25
-
26
- rows/cols:网格行数与列数;labels:"coords" 时格子内标注 (r,c),
27
- "empty" 时只画格线;origin 目前只支持 "top-left"(row 0 在上、
28
- col 0 在左)。axis 为 True 时画两条带箭头的轴:横轴 `col` 从
29
- 原点向右增长,纵轴 `row` 向下增长,轴与网格之间的数字是各列/
30
- 各行的下标;左上格外角画一个原点记号,箭头方向与原点位置直观
31
- 呈现「row 向下增长、原点在左上角」这两个程序事实。轴名与代码
32
- 字段 `Position::row` / `Position::col` 及 `kRows` / `kCols` 对应。
33
- marks:{(r,c): 文本},在对应格子居中放大绘制棋子等标记,
34
- 覆盖该格的坐标标注;highlight:(r,c),为该格加一圈加粗描边,
35
- 表达落子目标框。
36
- """
37
- if origin != "top-left":
38
- raise ValueError(f"不支持的 origin: {origin!r}")
39
- if rows < 1 or cols < 1:
40
- raise ValueError("rows 与 cols 至少为 1")
41
- if labels not in ("coords", "empty"):
42
- raise ValueError(f"不支持的 labels: {labels!r}")
43
- marks = dict(marks or {})
44
- for r, c in marks:
45
- if not (0 <= r < rows and 0 <= c < cols):
46
- raise ValueError(f"marks 越界: {(r, c)!r}")
47
- if highlight is not None:
48
- hr, hc = highlight
49
- if not (0 <= hr < rows and 0 <= hc < cols):
50
- raise ValueError(f"highlight 越界: {highlight!r}")
51
-
52
- # 轴几何:axis 开启时画带箭头的两条轴,轴名压在轴线上方/左方。
53
- axis_top = 18 if axis else 0
54
- axis_left = 18 if axis else 0
55
- arrow_pad = 18 # 箭头伸出网格外的留白
56
- grid_left = axis_left + label_gap
57
- grid_top = axis_top + label_gap
58
- grid_right = grid_left + cols * cell
59
- grid_bottom = grid_top + rows * cell
60
- width = grid_right + arrow_pad
61
- height = grid_bottom + arrow_pad
62
- parts = [
63
- f'<svg class="coords-grid" xmlns="http://www.w3.org/2000/svg" '
64
- f'viewBox="0 0 {width} {height}" width="{width}" height="{height}" '
65
- f'font-family="{FONT_FAMILY}" font-size="{font_size}">',
66
- '<style>.coords-grid{color:#1F2328}'
67
- '@media (prefers-color-scheme: dark){.coords-grid{color:#CDD9E5}}</style>',
68
- '<g stroke="currentColor" fill="none" stroke-width="1.25">',
69
- ]
70
-
71
- if axis:
72
- # 横轴:从原点侧向右延伸,箭头指右,表达 col 向右增长。
73
- parts.append(
74
- f'<line x1="{axis_left}" y1="{axis_top}" '
75
- f'x2="{grid_right + 10}" y2="{axis_top}"/>'
76
- )
77
- parts.append(
78
- f'<polygon points="{grid_right + 10},{axis_top - 4} '
79
- f'{grid_right + 18},{axis_top} '
80
- f'{grid_right + 10},{axis_top + 4}" '
81
- f'fill="currentColor" stroke="none"/>'
82
- )
83
- parts.append(
84
- f'<text x="{grid_left + cols * cell / 2}" y="{axis_top - 6}" '
85
- f'text-anchor="middle" '
86
- f'fill="currentColor" stroke="none">col</text>'
87
- )
88
- # 纵轴:从原点侧向下延伸,箭头指下,表达 row 向下增长。
89
- parts.append(
90
- f'<line x1="{axis_left}" y1="{axis_top}" '
91
- f'x2="{axis_left}" y2="{grid_bottom + 10}"/>'
92
- )
93
- parts.append(
94
- f'<polygon points="{axis_left - 4},{grid_bottom + 10} '
95
- f'{axis_left},{grid_bottom + 18} '
96
- f'{axis_left + 4},{grid_bottom + 10}" '
97
- f'fill="currentColor" stroke="none"/>'
98
- )
99
- center_y = axis_top + label_gap + rows * cell / 2
100
- parts.append(
101
- f'<text x="10" y="{center_y}" '
102
- f'transform="rotate(-90 10 {center_y})" text-anchor="middle" '
103
- f'fill="currentColor" stroke="none">row</text>'
104
- )
105
-
106
- for c in range(cols):
107
- x = grid_left + c * cell
108
- parts.append(
109
- f'<text x="{x + cell / 2}" y="{grid_top - 10}" '
110
- f'text-anchor="middle" fill="currentColor" stroke="none">{c}</text>'
111
- )
112
- for r in range(rows):
113
- y = grid_top + r * cell
114
- parts.append(
115
- f'<text x="{grid_left - 10}" y="{y + cell / 2}" '
116
- f'text-anchor="end" dominant-baseline="middle" '
117
- f'fill="currentColor" stroke="none">{r}</text>'
118
- )
119
-
120
- for r in range(rows):
121
- for c in range(cols):
122
- x = grid_left + c * cell
123
- y = grid_top + r * cell
124
- parts.append(
125
- f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}"/>'
126
- )
127
- if labels == "coords" and (r, c) not in marks:
128
- parts.append(
129
- f'<text x="{x + cell / 2}" y="{y + cell / 2}" '
130
- f'text-anchor="middle" dominant-baseline="middle" '
131
- f'fill="currentColor" stroke="none">({r},{c})</text>'
132
- )
133
-
134
- parts.append("</g>")
135
-
136
- # 标记、高亮与原点记号画在格线组外:棋子是内容不是格线,加粗描边
137
- # 与实心原点需要独立线宽和填充。
138
- if axis:
139
- parts.append(
140
- f'<circle cx="{grid_left}" cy="{grid_top}" r="3" '
141
- f'fill="currentColor"/>'
142
- )
143
- for (r, c), text in marks.items():
144
- x = grid_left + c * cell + cell / 2
145
- y = grid_top + r * cell + cell / 2
146
- parts.append(
147
- f'<text x="{x}" y="{y}" text-anchor="middle" '
148
- f'dominant-baseline="middle" font-size="{font_size * 2}" '
149
- f'fill="currentColor">{text}</text>'
150
- )
151
- if highlight is not None:
152
- hr, hc = highlight
153
- x = axis_left + label_gap + hc * cell
154
- y = axis_top + label_gap + hr * cell
155
- parts.append(
156
- f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" '
157
- f'fill="none" stroke="currentColor" stroke-width="3"/>'
158
- )
159
-
160
- parts.append("</svg>")
161
- return "\n".join(parts)
162
-
163
-
164
- def render_index_strip(count, cols=3, highlight=None, cell=56, font_size=14):
165
- """渲染一维下标与行列坐标的对应条带图,返回 SVG 字符串。
166
-
167
- count:数组长度;cols:每 cols 个下标分成一组,对应 kCols。
168
- 每组上方标 row = N 并画一条组线,格内标一维下标,格下标出换算
169
- 前的 (row, col);highlight 指定加粗的下标,用于标出公式示例
170
- (如 (1, 2) 换算出的 5)。越界参数直接抛 ValueError。
171
- """
172
- if count < 1:
173
- raise ValueError("count 至少为 1")
174
- if cols < 1:
175
- raise ValueError("cols 至少为 1")
176
- if highlight is not None and not 0 <= highlight < count:
177
- raise ValueError(f"highlight 越界: {highlight!r}")
178
-
179
- # 几何:每组上方留 row = N 标注行,格下留 (row, col) 标注行;
180
- # 组内格间距小、组间距大,隔开各行占用的下标段。
181
- gap_in = 8
182
- gap_group = 30
183
- label_h = 24
184
- below_h = 24
185
- pad = 6
186
- groups = (count + cols - 1) // cols
187
- width = (pad * 2 + count * cell
188
- + (count - groups) * gap_in + (groups - 1) * gap_group)
189
- height = pad * 2 + label_h + cell + below_h
190
- parts = [
191
- f'<svg class="coords-grid" xmlns="http://www.w3.org/2000/svg" '
192
- f'viewBox="0 0 {width} {height}" width="{width}" height="{height}" '
193
- f'font-family="{FONT_FAMILY}" font-size="{font_size}">',
194
- '<style>.coords-grid{color:#1F2328}'
195
- '@media (prefers-color-scheme: dark){.coords-grid{color:#CDD9E5}}</style>',
196
- '<g stroke="currentColor" fill="none" stroke-width="1.25">',
197
- ]
198
-
199
- def group_left(row):
200
- """返回第 row 组第一个格子的左边缘 x 坐标。"""
201
- x = pad
202
- for r in range(row):
203
- n_prev = min(cols, count - r * cols)
204
- x += n_prev * cell + (n_prev - 1) * gap_in + gap_group
205
- return x
206
-
207
- for row in range(groups):
208
- n_in_group = min(cols, count - row * cols)
209
- left = group_left(row)
210
- right = left + (n_in_group - 1) * (cell + gap_in) + cell
211
- center = (left + right) / 2
212
- parts.append(
213
- f'<text x="{center}" y="{pad + 13}" text-anchor="middle" '
214
- f'fill="currentColor" stroke="none">row = {row}</text>'
215
- )
216
- parts.append(
217
- f'<line x1="{left}" y1="{pad + label_h - 5}" '
218
- f'x2="{right}" y2="{pad + label_h - 5}"/>'
219
- )
220
-
221
- for index in range(count):
222
- row, col = divmod(index, cols)
223
- x = group_left(row) + col * (cell + gap_in)
224
- y = pad + label_h
225
- parts.append(f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}"/>')
226
- parts.append(
227
- f'<text x="{x + cell / 2}" y="{y + cell / 2}" '
228
- f'text-anchor="middle" dominant-baseline="middle" '
229
- f'font-size="{font_size + 2}" '
230
- f'fill="currentColor" stroke="none">{index}</text>'
231
- )
232
- parts.append(
233
- f'<text x="{x + cell / 2}" y="{y + cell + 16}" '
234
- f'text-anchor="middle" font-size="{font_size - 2}" '
235
- f'fill="currentColor" stroke="none">({row},{col})</text>'
236
- )
237
-
238
- parts.append("</g>")
239
-
240
- if highlight is not None:
241
- row, col = divmod(highlight, cols)
242
- x = group_left(row) + col * (cell + gap_in)
243
- y = pad + label_h
244
- parts.append(
245
- f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" '
246
- f'fill="none" stroke="currentColor" stroke-width="3"/>'
247
- )
248
-
249
- parts.append("</svg>")
250
- return "\n".join(parts)
1
+ #!/usr/bin/env python3
2
+ """rendering-coords.py - 坐标轴网格图的共享 SVG 绘图助手。
3
+
4
+ Quarto 章节里的 ``{python}`` 可执行单元调用本助手生成坐标轴网格图
5
+ (棋盘行列、二维数组下标等),绘图逻辑只存在于本文件;正文单元应
6
+ 只含调用,不内联第二套 SVG 逻辑(口径见 `quarto/coords.md`)。只用标准库拼 SVG;描边与文字用 currentColor,跟随
7
+ 页面明暗主题,不写死颜色。
8
+ """
9
+
10
+ #===------------------------------------------------------------------------===#
11
+ # 常量
12
+ #===------------------------------------------------------------------------===#
13
+
14
+ FONT_FAMILY = "LXGW Bright Medium, system-ui, sans-serif"
15
+
16
+
17
+ #===------------------------------------------------------------------------===#
18
+ # 绘图
19
+ #===------------------------------------------------------------------------===#
20
+
21
+ def render_grid(rows=3, cols=3, labels="coords", origin="top-left",
22
+ cell=64, label_gap=30, font_size=14, marks=None,
23
+ highlight=None, axis=True):
24
+ """渲染一张程序坐标系网格图,返回 SVG 字符串。
25
+
26
+ rows/cols:网格行数与列数;labels:"coords" 时格子内标注 (r,c),
27
+ "empty" 时只画格线;origin 目前只支持 "top-left"(row 0 在上、
28
+ col 0 在左)。axis 为 True 时画两条带箭头的轴:横轴 `col` 从
29
+ 原点向右增长,纵轴 `row` 向下增长,轴与网格之间的数字是各列/
30
+ 各行的下标;左上格外角画一个原点记号,箭头方向与原点位置直观
31
+ 呈现「row 向下增长、原点在左上角」这两个程序事实。轴名与代码
32
+ 字段 `Position::row` / `Position::col` 及 `kRows` / `kCols` 对应。
33
+ marks:{(r,c): 文本},在对应格子居中放大绘制棋子等标记,
34
+ 覆盖该格的坐标标注;highlight:(r,c),为该格加一圈加粗描边,
35
+ 表达落子目标框。
36
+ """
37
+ if origin != "top-left":
38
+ raise ValueError(f"不支持的 origin: {origin!r}")
39
+ if rows < 1 or cols < 1:
40
+ raise ValueError("rows 与 cols 至少为 1")
41
+ if labels not in ("coords", "empty"):
42
+ raise ValueError(f"不支持的 labels: {labels!r}")
43
+ marks = dict(marks or {})
44
+ for r, c in marks:
45
+ if not (0 <= r < rows and 0 <= c < cols):
46
+ raise ValueError(f"marks 越界: {(r, c)!r}")
47
+ if highlight is not None:
48
+ hr, hc = highlight
49
+ if not (0 <= hr < rows and 0 <= hc < cols):
50
+ raise ValueError(f"highlight 越界: {highlight!r}")
51
+
52
+ # 轴几何:axis 开启时画带箭头的两条轴,轴名压在轴线上方/左方。
53
+ axis_top = 18 if axis else 0
54
+ axis_left = 18 if axis else 0
55
+ arrow_pad = 18 # 箭头伸出网格外的留白
56
+ grid_left = axis_left + label_gap
57
+ grid_top = axis_top + label_gap
58
+ grid_right = grid_left + cols * cell
59
+ grid_bottom = grid_top + rows * cell
60
+ width = grid_right + arrow_pad
61
+ height = grid_bottom + arrow_pad
62
+ parts = [
63
+ f'<svg class="coords-grid" xmlns="http://www.w3.org/2000/svg" '
64
+ f'viewBox="0 0 {width} {height}" width="{width}" height="{height}" '
65
+ f'font-family="{FONT_FAMILY}" font-size="{font_size}">',
66
+ '<style>.coords-grid{color:#1F2328}'
67
+ '@media (prefers-color-scheme: dark){.coords-grid{color:#CDD9E5}}</style>',
68
+ '<g stroke="currentColor" fill="none" stroke-width="1.25">',
69
+ ]
70
+
71
+ if axis:
72
+ # 横轴:从原点侧向右延伸,箭头指右,表达 col 向右增长。
73
+ parts.append(
74
+ f'<line x1="{axis_left}" y1="{axis_top}" '
75
+ f'x2="{grid_right + 10}" y2="{axis_top}"/>'
76
+ )
77
+ parts.append(
78
+ f'<polygon points="{grid_right + 10},{axis_top - 4} '
79
+ f'{grid_right + 18},{axis_top} '
80
+ f'{grid_right + 10},{axis_top + 4}" '
81
+ f'fill="currentColor" stroke="none"/>'
82
+ )
83
+ parts.append(
84
+ f'<text x="{grid_left + cols * cell / 2}" y="{axis_top - 6}" '
85
+ f'text-anchor="middle" '
86
+ f'fill="currentColor" stroke="none">col</text>'
87
+ )
88
+ # 纵轴:从原点侧向下延伸,箭头指下,表达 row 向下增长。
89
+ parts.append(
90
+ f'<line x1="{axis_left}" y1="{axis_top}" '
91
+ f'x2="{axis_left}" y2="{grid_bottom + 10}"/>'
92
+ )
93
+ parts.append(
94
+ f'<polygon points="{axis_left - 4},{grid_bottom + 10} '
95
+ f'{axis_left},{grid_bottom + 18} '
96
+ f'{axis_left + 4},{grid_bottom + 10}" '
97
+ f'fill="currentColor" stroke="none"/>'
98
+ )
99
+ center_y = axis_top + label_gap + rows * cell / 2
100
+ parts.append(
101
+ f'<text x="10" y="{center_y}" '
102
+ f'transform="rotate(-90 10 {center_y})" text-anchor="middle" '
103
+ f'fill="currentColor" stroke="none">row</text>'
104
+ )
105
+
106
+ for c in range(cols):
107
+ x = grid_left + c * cell
108
+ parts.append(
109
+ f'<text x="{x + cell / 2}" y="{grid_top - 10}" '
110
+ f'text-anchor="middle" fill="currentColor" stroke="none">{c}</text>'
111
+ )
112
+ for r in range(rows):
113
+ y = grid_top + r * cell
114
+ parts.append(
115
+ f'<text x="{grid_left - 10}" y="{y + cell / 2}" '
116
+ f'text-anchor="end" dominant-baseline="middle" '
117
+ f'fill="currentColor" stroke="none">{r}</text>'
118
+ )
119
+
120
+ for r in range(rows):
121
+ for c in range(cols):
122
+ x = grid_left + c * cell
123
+ y = grid_top + r * cell
124
+ parts.append(
125
+ f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}"/>'
126
+ )
127
+ if labels == "coords" and (r, c) not in marks:
128
+ parts.append(
129
+ f'<text x="{x + cell / 2}" y="{y + cell / 2}" '
130
+ f'text-anchor="middle" dominant-baseline="middle" '
131
+ f'fill="currentColor" stroke="none">({r},{c})</text>'
132
+ )
133
+
134
+ parts.append("</g>")
135
+
136
+ # 标记、高亮与原点记号画在格线组外:棋子是内容不是格线,加粗描边
137
+ # 与实心原点需要独立线宽和填充。
138
+ if axis:
139
+ parts.append(
140
+ f'<circle cx="{grid_left}" cy="{grid_top}" r="3" '
141
+ f'fill="currentColor"/>'
142
+ )
143
+ for (r, c), text in marks.items():
144
+ x = grid_left + c * cell + cell / 2
145
+ y = grid_top + r * cell + cell / 2
146
+ parts.append(
147
+ f'<text x="{x}" y="{y}" text-anchor="middle" '
148
+ f'dominant-baseline="middle" font-size="{font_size * 2}" '
149
+ f'fill="currentColor">{text}</text>'
150
+ )
151
+ if highlight is not None:
152
+ hr, hc = highlight
153
+ x = axis_left + label_gap + hc * cell
154
+ y = axis_top + label_gap + hr * cell
155
+ parts.append(
156
+ f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" '
157
+ f'fill="none" stroke="currentColor" stroke-width="3"/>'
158
+ )
159
+
160
+ parts.append("</svg>")
161
+ return "\n".join(parts)
162
+
163
+
164
+ def render_index_strip(count, cols=3, highlight=None, cell=56, font_size=14):
165
+ """渲染一维下标与行列坐标的对应条带图,返回 SVG 字符串。
166
+
167
+ count:数组长度;cols:每 cols 个下标分成一组,对应 kCols。
168
+ 每组上方标 row = N 并画一条组线,格内标一维下标,格下标出换算
169
+ 前的 (row, col);highlight 指定加粗的下标,用于标出公式示例
170
+ (如 (1, 2) 换算出的 5)。越界参数直接抛 ValueError。
171
+ """
172
+ if count < 1:
173
+ raise ValueError("count 至少为 1")
174
+ if cols < 1:
175
+ raise ValueError("cols 至少为 1")
176
+ if highlight is not None and not 0 <= highlight < count:
177
+ raise ValueError(f"highlight 越界: {highlight!r}")
178
+
179
+ # 几何:每组上方留 row = N 标注行,格下留 (row, col) 标注行;
180
+ # 组内格间距小、组间距大,隔开各行占用的下标段。
181
+ gap_in = 8
182
+ gap_group = 30
183
+ label_h = 24
184
+ below_h = 24
185
+ pad = 6
186
+ groups = (count + cols - 1) // cols
187
+ width = (pad * 2 + count * cell
188
+ + (count - groups) * gap_in + (groups - 1) * gap_group)
189
+ height = pad * 2 + label_h + cell + below_h
190
+ parts = [
191
+ f'<svg class="coords-grid" xmlns="http://www.w3.org/2000/svg" '
192
+ f'viewBox="0 0 {width} {height}" width="{width}" height="{height}" '
193
+ f'font-family="{FONT_FAMILY}" font-size="{font_size}">',
194
+ '<style>.coords-grid{color:#1F2328}'
195
+ '@media (prefers-color-scheme: dark){.coords-grid{color:#CDD9E5}}</style>',
196
+ '<g stroke="currentColor" fill="none" stroke-width="1.25">',
197
+ ]
198
+
199
+ def group_left(row):
200
+ """返回第 row 组第一个格子的左边缘 x 坐标。"""
201
+ x = pad
202
+ for r in range(row):
203
+ n_prev = min(cols, count - r * cols)
204
+ x += n_prev * cell + (n_prev - 1) * gap_in + gap_group
205
+ return x
206
+
207
+ for row in range(groups):
208
+ n_in_group = min(cols, count - row * cols)
209
+ left = group_left(row)
210
+ right = left + (n_in_group - 1) * (cell + gap_in) + cell
211
+ center = (left + right) / 2
212
+ parts.append(
213
+ f'<text x="{center}" y="{pad + 13}" text-anchor="middle" '
214
+ f'fill="currentColor" stroke="none">row = {row}</text>'
215
+ )
216
+ parts.append(
217
+ f'<line x1="{left}" y1="{pad + label_h - 5}" '
218
+ f'x2="{right}" y2="{pad + label_h - 5}"/>'
219
+ )
220
+
221
+ for index in range(count):
222
+ row, col = divmod(index, cols)
223
+ x = group_left(row) + col * (cell + gap_in)
224
+ y = pad + label_h
225
+ parts.append(f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}"/>')
226
+ parts.append(
227
+ f'<text x="{x + cell / 2}" y="{y + cell / 2}" '
228
+ f'text-anchor="middle" dominant-baseline="middle" '
229
+ f'font-size="{font_size + 2}" '
230
+ f'fill="currentColor" stroke="none">{index}</text>'
231
+ )
232
+ parts.append(
233
+ f'<text x="{x + cell / 2}" y="{y + cell + 16}" '
234
+ f'text-anchor="middle" font-size="{font_size - 2}" '
235
+ f'fill="currentColor" stroke="none">({row},{col})</text>'
236
+ )
237
+
238
+ parts.append("</g>")
239
+
240
+ if highlight is not None:
241
+ row, col = divmod(highlight, cols)
242
+ x = group_left(row) + col * (cell + gap_in)
243
+ y = pad + label_h
244
+ parts.append(
245
+ f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" '
246
+ f'fill="none" stroke="currentColor" stroke-width="3"/>'
247
+ )
248
+
249
+ parts.append("</svg>")
250
+ return "\n".join(parts)
@@ -1,71 +1,71 @@
1
- #!/usr/bin/env python3
2
- """Self-test for checking-names.py.
3
-
4
- The test imports the sibling module and asserts the accepted and rejected name
5
- forms, including ASCII-only, hyphen and tool-fixed-name cases. It runs without
6
- a book checkout.
7
-
8
- Usage:
9
- python testing-check-ascii-names.py
10
- Exit code: 0 when every case passes, 1 otherwise.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import importlib.util
16
- import sys
17
- from pathlib import Path
18
-
19
- HERE = Path(__file__).resolve().parent
20
-
21
-
22
- def load_module():
23
- spec = importlib.util.spec_from_file_location("checking_names", HERE / "checking-names.py")
24
- if spec is None or spec.loader is None:
25
- raise RuntimeError("无法加载 checking-names.py")
26
- module = importlib.util.module_from_spec(spec)
27
- spec.loader.exec_module(module)
28
- return module
29
-
30
-
31
- def main() -> int:
32
- try:
33
- sys.stdout.reconfigure(encoding="utf-8")
34
- except Exception:
35
- pass
36
-
37
- module = load_module()
38
- failures: list[str] = []
39
-
40
- accepted = ("designing-theme", "01-rules.qmd", "theme-light.scss", "v1.2.md")
41
- rejected = ("board_test.cpp", "test support.md", "Designing-theme", "a--b.md")
42
-
43
- for name in accepted:
44
- error = module.name_form_error(name)
45
- if error is not None:
46
- failures.append(f"{name} 应通过,实际失败:{error}")
47
-
48
- for name in rejected:
49
- if module.name_form_error(name) is None:
50
- failures.append(f"{name} 应失败,实际通过")
51
-
52
- for tool_name in ("CMakeLists.txt", "SKILL.md", "README.en.md", "package.json"):
53
- error = module.name_form_error(tool_name)
54
- if error is not None:
55
- failures.append(f"工具固定名 {tool_name} 应通过,实际失败:{error}")
56
-
57
- if module.first_non_ascii("中文.md") is None:
58
- failures.append("first_non_ascii 未识别中文字符")
59
-
60
- if failures:
61
- print(f"发现 {len(failures)} 例失败:")
62
- for line in failures:
63
- print(f" FAIL {line}")
64
- return 1
65
-
66
- print(f"PASS:{len(accepted) + len(rejected) + 4} 个用例全部通过")
67
- return 0
68
-
69
-
70
- if __name__ == "__main__":
71
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """Self-test for checking-names.py.
3
+
4
+ The test imports the sibling module and asserts the accepted and rejected name
5
+ forms, including ASCII-only, hyphen and tool-fixed-name cases. It runs without
6
+ a book checkout.
7
+
8
+ Usage:
9
+ python testing-check-ascii-names.py
10
+ Exit code: 0 when every case passes, 1 otherwise.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.util
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ HERE = Path(__file__).resolve().parent
20
+
21
+
22
+ def load_module():
23
+ spec = importlib.util.spec_from_file_location("checking_names", HERE / "checking-names.py")
24
+ if spec is None or spec.loader is None:
25
+ raise RuntimeError("无法加载 checking-names.py")
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ def main() -> int:
32
+ try:
33
+ sys.stdout.reconfigure(encoding="utf-8")
34
+ except Exception:
35
+ pass
36
+
37
+ module = load_module()
38
+ failures: list[str] = []
39
+
40
+ accepted = ("designing-theme", "01-rules.qmd", "theme-light.scss", "v1.2.md")
41
+ rejected = ("board_test.cpp", "test support.md", "Designing-theme", "a--b.md")
42
+
43
+ for name in accepted:
44
+ error = module.name_form_error(name)
45
+ if error is not None:
46
+ failures.append(f"{name} 应通过,实际失败:{error}")
47
+
48
+ for name in rejected:
49
+ if module.name_form_error(name) is None:
50
+ failures.append(f"{name} 应失败,实际通过")
51
+
52
+ for tool_name in ("CMakeLists.txt", "SKILL.md", "README.en.md", "package.json"):
53
+ error = module.name_form_error(tool_name)
54
+ if error is not None:
55
+ failures.append(f"工具固定名 {tool_name} 应通过,实际失败:{error}")
56
+
57
+ if module.first_non_ascii("中文.md") is None:
58
+ failures.append("first_non_ascii 未识别中文字符")
59
+
60
+ if failures:
61
+ print(f"发现 {len(failures)} 例失败:")
62
+ for line in failures:
63
+ print(f" FAIL {line}")
64
+ return 1
65
+
66
+ print(f"PASS:{len(accepted) + len(rejected) + 4} 个用例全部通过")
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ sys.exit(main())