13b3ebbe14
把川大 wiki「02.Qt方向」课程代码整理为 CMake 管理的可编译运行项目。
Part1 C/C++ 强化 (p01/, 144 目标, host gcc/g++ 构建, 全绿):
- 修复全部编译失败:补 <cstring>/<fstream>/<iomanip>/<vector> 等缺失头
(common_fix + infer_stl_includes 自动检测注入)、修正 pause() 壳注入
(改检测已转换后的 pause 调用)、namespace 提升出函数体、const 成员函数
正确性、dedupe 不再破坏函数重载集、K&R 隐式 int / 动态异常规范等
C++17 移除特性的可编译等价改写。
- 目录采用深缩写 p01/ch04/s03/ (原 part1_cpp/ch04_class_object/03_ctor_dtor)。
Part3 Qt 桌面 (p03/, 29 目标, 隔离 Qt 5.14.2 构建, 全绿):
- gen_part3.py: Qt 外壳模板 (main+QApplication+show, QTimer 自动退出便于
offscreen 验证)、AUTOMOC + .moc include、rewrite_* 处理散文混入/重载/
资源依赖、生成占位 png/gif + .qrc + rundata 样例数据。
- 修正 QLable 笔误、全角分号、.→->、Windows 路径、QApplication 阻塞事件
循环改为 QCoreApplication 等。
任务1 概念/版本验证:
- tools/std_probe.py: 跨 -std= 编译探针 (--pedantic-errors 让已废除特性硬失败)。
- tools/demo_versions/: K&R 隐式 int、三目左值、void* 转换、enum 赋整、
const 经指针、动态异常规范 共 6 个跨版本演示。
- docs/VERSION_NOTES.md: 每条「原文说法→C17/C++17 是否成立→何版本成立→已验证」。
文档: 顶层 README、docs/SOURCE_PAGES.md (页面→目录映射)、
docs/ERRATA.md (Part1 修正)、docs/ERRATA_part3.md (Part3 修正)。
另修复 tools/run.sh / run_qt.sh 的 ${1:?...} 引号语法 bug。
隔离 Qt 验证: ldd 0 系统 Qt 引用; 173/173 目标全绿; p03 offscreen 运行通过。
321 lines
17 KiB
Python
321 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
# ==============================================================================
|
||
# gen_sources.py — 把抓取的 wiki 数据生成为课程源码 + CMakeLists(可复现)
|
||
#
|
||
# 输入 : tools/wiki_data/{extracted_code.json, all_page_ids.json,
|
||
# qt_full_tree.json, bodies/*.json}
|
||
# 输出 : part1_cpp/**, part3_qt_desktop/** 的 .c/.cpp/.h/.qrc 与 CMakeLists.txt
|
||
#
|
||
# 设计要点(与计划一致):
|
||
# * 按页面标题层级(h1..h6)解析子话题;同一子话题内的代码块(多为 test()
|
||
# 片段)合并为「一个完整小程序」,把各片段按教学顺序嵌入,并标注出处。
|
||
# * C/C++ 按内容上下文判定:用 gcc -std=c17 或 g++ -std=c++17 构建。
|
||
# * 每个完整小程序 = 一个独立 add_executable 目标,可单独构建运行。
|
||
#
|
||
# 本文件是「骨架/分类/合并」的通用引擎;针对每个页面需要的人工修正
|
||
# (笔误、system("pause")、缺 include、伪代码等)通过 OVERRIDES 字典注入,
|
||
# 并自动记录到 docs/ERRATA.md,使生成过程透明可审计。
|
||
# ------------------------------------------------------------------------------
|
||
import json, os, re, glob, sys
|
||
from html.parser import HTMLParser
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
WIKI = os.path.join(REPO, "tools", "wiki_data")
|
||
|
||
# ------------------------------------------------------------------ 标题解析 ----
|
||
class OutlineParser(HTMLParser):
|
||
"""顺序产出 ('h', level, text) 与 ('code', idx) 事件,定位每个代码块所属标题。"""
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.events = []
|
||
self.code_depth = 0
|
||
self.code_idx = 0
|
||
self.in_code_body = False
|
||
self.capture_h = False
|
||
self.cur_lvl = None
|
||
self.cur_buf = []
|
||
def handle_starttag(self, tag, attrs):
|
||
ad = dict(attrs)
|
||
if tag in ("h1","h2","h3","h4","h5","h6"):
|
||
self.capture_h = True; self.cur_lvl = int(tag[1]); self.cur_buf = []
|
||
elif tag in ("ac:structured-macro","structured-macro") and (
|
||
ad.get("ac:name") == "code" or ad.get("name") == "code"):
|
||
self.code_depth += 1
|
||
def handle_endtag(self, tag):
|
||
if tag in ("h1","h2","h3","h4","h5","h6") and self.capture_h:
|
||
txt = re.sub(r"\s+", " ", "".join(self.cur_buf)).strip()
|
||
self.events.append(("h", self.cur_lvl, txt))
|
||
self.capture_h = False
|
||
elif tag in ("ac:structured-macro","structured-macro") and self.code_depth >= 1:
|
||
if self.code_depth == 1:
|
||
self.events.append(("code", self.code_idx))
|
||
self.code_idx += 1
|
||
self.code_depth -= 1
|
||
def unknown_decl(self, decl):
|
||
pass # CDATA handled by the code extractor separately
|
||
def handle_data(self, data):
|
||
if self.capture_h:
|
||
self.cur_buf.append(data)
|
||
def handle_entityref(self, name):
|
||
if self.capture_h:
|
||
self.cur_buf.append({"nbsp": " "}.get(name, f"&{name};"))
|
||
|
||
def page_outline(body):
|
||
"""Return list of {level, title, code_indices} for the deepest headings that
|
||
own at least one code block, in document order. Each code block is attached
|
||
to the heading under which it physically appears (last heading seen above it
|
||
at any level). Headings with no code blocks are dropped from this list but
|
||
their text is folded into the preceding owning heading's context."""
|
||
if not body:
|
||
return []
|
||
p = OutlineParser()
|
||
p.feed(body)
|
||
owning = [] # stack of (level, title) currently open
|
||
sections = [] # list of dict
|
||
cur_section = None
|
||
for ev in p.events:
|
||
if ev[0] == "h":
|
||
_, lvl, txt = ev
|
||
# pop closed headings
|
||
while owning and owning[-1][0] >= lvl:
|
||
owning.pop()
|
||
owning.append((lvl, txt))
|
||
cur_section = None # a new heading starts a new (potential) section
|
||
else: # code
|
||
if cur_section is None:
|
||
# build a context title from the heading path
|
||
path_titles = [t for (_, t) in owning]
|
||
title = path_titles[-1] if path_titles else "(导言)"
|
||
cur_section = {"title": title, "path": path_titles, "code_indices": []}
|
||
sections.append(cur_section)
|
||
cur_section["code_indices"].append(ev[1])
|
||
return sections
|
||
|
||
# ------------------------------------------------------------------ 语言判定 ----
|
||
CPP_TOKENS = ["iostream", "std::", "#include <iostream>", "#include<iostream>",
|
||
"class ", "template", "namespace", "using namespace", "new ",
|
||
"delete ", "std::string", "cout", "cin", "cerr", "vector<",
|
||
"string ", "->", "const_cast", "static_cast", "dynamic_cast",
|
||
"reinterpret_cast", "operator ", "public:", "private:", "protected:",
|
||
"signals:", "slots:", "virtual ", "throw ", "catch ", "try {",
|
||
"Q_OBJECT", "QApplication", "QWidget", "emit ", "qDebug", "QFile",
|
||
"#include <Q", "#include<Q"]
|
||
C_TOKENS = ["printf", "scanf", "puts(", "getchar", "malloc", "free(",
|
||
"#include <stdio.h>", "#include<stdio.h>", "#include <stdlib.h>",
|
||
"#include <string.h>", "#include <math.h>", "strcpy", "strcat",
|
||
"strcmp", "sprintf", "EXIT_SUCCESS"]
|
||
|
||
def classify_lang(code):
|
||
"""Return 'c' | 'cpp' | 'qt' based on content."""
|
||
is_qt = any(t in code for t in
|
||
["Q_OBJECT","QApplication","QWidget","QMainWindow","QDialog","QPushButton",
|
||
"QLabel","#include <Q","#include<Q","emit ","qDebug","QFile","QPainter",
|
||
"QMessageBox","QHBoxLayout","QVBoxLayout","QGridLayout","QLineEdit",
|
||
"QTextEdit","QComboBox","QEvent","QPaintEvent","QTimer","QAction",
|
||
"QMenuBar","QStatusBar","QToolBar","QFileDialog","QTextStream",
|
||
"QDataStream","QDir","QFileInfo","QSpinBox","QSlider","QDockWidget",
|
||
"QTextEdit","QPixmap","QImage","QPicture","QBitmap","QMovie"])
|
||
if is_qt:
|
||
return "qt"
|
||
is_cpp = any(t in code for t in CPP_TOKENS)
|
||
is_c = any(t in code for t in C_TOKENS)
|
||
if is_cpp:
|
||
return "cpp"
|
||
if is_c:
|
||
return "c"
|
||
return "cpp" # default to C++ when ambiguous (most wiki blocks are C++)
|
||
|
||
def is_complete(code):
|
||
return bool(re.search(r"\bint\s+main\s*\(", code))
|
||
|
||
def has_windows_pause(code):
|
||
return 'system("pause")' in code or 'system( "pause" )' in code
|
||
|
||
# C 字符串/内存函数(<cstring>/<string.h> 提供)。wiki 的 C++ 片段常直接用 strcpy/
|
||
# strlen 等,却未 include 对应头文件(原意依赖教学环境的传递包含)。检测到即补头。
|
||
CSTRING_RE = re.compile(
|
||
r"\b(strcpy|strncpy|strcat|strncat|strlen|strcmp|strncmp|strchr|strrchr|"
|
||
r"strstr|strtok|strerror|strcoll|memcpy|memmove|memset|memcmp|memchr|"
|
||
r"sprintf|snprintf)\s*\(")
|
||
|
||
def needs_cstring(code):
|
||
"""True if the fragment calls any <cstring> function and so needs the header."""
|
||
return bool(CSTRING_RE.search(code))
|
||
|
||
def has_pause_call(code):
|
||
"""True if the fragment calls pause() (after common_fix converted system("pause"))."""
|
||
return bool(re.search(r"\bpause\s*\(", code))
|
||
|
||
# Map of (header) → list of tokens that, when used unqualified (relying on
|
||
# `using namespace std;`), require that header. wiki STL/IO snippets routinely
|
||
# omit these includes (they rely on the teaching toolchain's transitive pulls).
|
||
# Detection is on bare symbol usage; includes are injected by the emit template.
|
||
_STL_HEADER_TOKENS = {
|
||
"<vector>": [r"\bvector\b"],
|
||
"<list>": [r"\blist\b"],
|
||
"<deque>": [r"\bdeque\b"],
|
||
"<set>": [r"\bset\s*<", r"\bmultiset\s*<"],
|
||
"<map>": [r"\bmap\s*<", r"\bmultimap\s*<"],
|
||
"<unordered_map>": [r"\bunordered_map\s*<"],
|
||
"<unordered_set>": [r"\bunordered_set\s*<"],
|
||
"<stack>": [r"\bstack\s*<"],
|
||
"<queue>": [r"\bqueue\s*<", r"\bpriority_queue\s*<"],
|
||
"<algorithm>":[r"\bfor_each\b", r"\btransform\b", r"\bfind\b", r"\bfind_if\b",
|
||
r"\bsort\b", r"\bcopy\b", r"\bremove\b", r"\bcount\b",
|
||
r"\bcount_if\b", r"\baccumulate\b", r"\bfill\b", r"\breplace\b",
|
||
r"\bswap\b", r"\breverse\b", r"\bmerge\b", r"\bunique\b",
|
||
r"\bbind\b"],
|
||
"<functional>":[r"\bfunction\s*<", r"\bplus\b", r"\bminus\b", r"\bmultiplies\b",
|
||
r"\bnegate\b", r"\bequal_to\b", r"\bless\b", r"\bgreater\b",
|
||
r"\blogical_and\b", r"\bplaceholders::_"],
|
||
"<numeric>": [r"\baccumulate\b"],
|
||
"<fstream>": [r"\bifstream\b", r"\bofstream\b", r"\bfstream\b"],
|
||
"<iomanip>": [r"\bsetw\b", r"\bsetfill\b", r"\bsetiosflags\b", r"\bresetiosflags\b",
|
||
r"\bsetbase\b", r"\bsetprecision\b"],
|
||
"<string>": [r"\bstd::string\b", r"\bgetline\b"],
|
||
"<cmath>": [r"\bsqrt\b", r"\bpow\b", r"\bsin\b", r"\bcos\b", r"\babs\b",
|
||
r"\bfabs\b", r"\bfloor\b", r"\bceil\b", r"\blog\b", r"\bexp\b"],
|
||
}
|
||
|
||
def infer_stl_includes(code):
|
||
"""Return the sorted list of C++ standard-library headers the fragment needs
|
||
but the wiki omitted (detected by symbol usage). Returns [] for C code."""
|
||
needed = []
|
||
for hdr, pats in _STL_HEADER_TOKENS.items():
|
||
if any(re.search(p, code) for p in pats):
|
||
needed.append(hdr)
|
||
# de-dup & sort for stable output
|
||
return sorted(set(needed))
|
||
|
||
# ------------------------------------------------------------------ 目标命名 ----
|
||
def slug(s):
|
||
"""Make a filesystem-friendly slug from a heading (keeps CJK for readability
|
||
of source filenames)."""
|
||
s = re.sub(r"^\d+(\.\d+)*\s*", "", s) # drop leading "3.10.2 "
|
||
s = re.sub(r"[((].*?[))]", "", s) # drop parenthetical
|
||
s = re.sub(r"[^\w\u4e00-\u9fff]+", "_", s).strip("_")
|
||
s = re.sub(r"_+", "_", s)
|
||
return s[:40] or "topic"
|
||
|
||
def ascii_slug(s):
|
||
"""ASCII-only slug for CMake target names (CMake forbids non-ASCII targets).
|
||
CJK chars are dropped; the result is guaranteed [A-Za-z0-9_]."""
|
||
s = re.sub(r"^\d+(\.\d+)*\s*", "", s)
|
||
s = re.sub(r"[((].*?[))]", "", s)
|
||
# drop anything that's not ASCII word char
|
||
s = re.sub(r"[^A-Za-z0-9_]+", "_", s).strip("_")
|
||
s = re.sub(r"_+", "_", s)
|
||
return s.lower()[:30] or "topic"
|
||
|
||
# ------------------------------------------------------------------ 输出助手 ----
|
||
def write(path, content):
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
f.write(content)
|
||
|
||
ERRATA = [] # list of (pageId, blockIdx, kind, before, after, reason)
|
||
|
||
def record_erratum(pid, idx, kind, before, after, reason):
|
||
ERRATA.append((pid, idx, kind, before, after, reason))
|
||
|
||
# ==============================================================================
|
||
# 公共修正:对「所有」part1 代码块都适用的轻量修补(保留原意、可移植)
|
||
# ==============================================================================
|
||
def common_fix(code, pid, idx):
|
||
orig = code
|
||
# 0) Normalize non-breaking spaces (U+00A0 NBSP) that the wiki editor injected
|
||
# into indented code — these are 'stray \302' compile errors otherwise.
|
||
if "\u00a0" in code:
|
||
code = code.replace("\u00a0", " ")
|
||
# 1) system("pause") → 便携 pause() 壳(仅当出现时;壳函数由模板注入)
|
||
if has_windows_pause(code):
|
||
code = re.sub(r'system\(\s*"pause"\s*\)', 'pause()', code)
|
||
code = re.sub(r'system\(\s*" pause"\s*\)', 'pause()', code)
|
||
record_erratum(pid, idx, "platform",
|
||
'system("pause")', 'pause()',
|
||
"system(\"pause\") 仅 Windows 可用;改用仓库提供的便携 pause() 壳函数 "
|
||
"(读一行 stdin 等待回车),行为一致,Linux 可编译运行。")
|
||
# 2) 全角分号 → 半角
|
||
if ";" in code:
|
||
code = code.replace(";", ";")
|
||
record_erratum(pid, idx, "typo", ";(全角分号)", ";(半角)",
|
||
"全角分号在 C/C++ 中非法,统一替换为半角。")
|
||
# 3) 全角引号 → 半角(教学代码常见误输)
|
||
for fq, hq in [("“", '"'), ("”", '"'), ("‘", "'"), ("’", "'")]:
|
||
if fq in code:
|
||
code = code.replace(fq, hq)
|
||
record_erratum(pid, idx, "typo", f"{fq}(全角引号)", f"{hq}(半角)",
|
||
"字符串/字符字面量须用半角引号。")
|
||
# 4) C 字符串/内存函数缺头:wiki 的 C++ 片段常直接调用 strcpy/strlen/strcmp 等
|
||
# 却未 include(依赖教学环境的传递包含)。检测到即补 <cstring>(C++)。
|
||
# include 由 emit 模板注入;此处只记录 erratum,避免与模板重复插入。
|
||
if needs_cstring(code):
|
||
record_erratum(pid, idx, "missing-include",
|
||
"调用 strcpy/strlen/strcmp 等 C 字符串函数但未 #include <cstring>",
|
||
"由生成模板统一补 #include <cstring>",
|
||
"C 字符串/内存函数声明在 <cstring>(C++)/ <string.h>(C);"
|
||
"原 wiki 代码依赖教学环境传递包含,隔离编译时须显式 include。")
|
||
# 5) STL 容器/算法/IO 头缺失:wiki 片段依赖 `using namespace std;` 且假定教学
|
||
# 工具链传递包含了 <vector>/<algorithm>/<fstream>/<iomanip> 等。隔离编译时这些
|
||
# 符号未声明。检测到的头由 emit 模板注入;此处只记录 erratum。
|
||
missing = infer_stl_includes(code)
|
||
if missing:
|
||
record_erratum(pid, idx, "missing-include",
|
||
f"使用 STL/IO 符号(如 vector/for_each/ifstream/setw 等)但未 #include {', '.join(missing)}",
|
||
f"由生成模板统一补 #include {' '.join(missing)}",
|
||
"原 wiki 代码假定教学环境已传递包含相应标准库头;按「示例相对隔离」独立编译"
|
||
"时须显式 include。检测依据为符号使用(见 gen_sources.infer_stl_includes)。")
|
||
return code
|
||
|
||
# ---------------------------------------------------------------- 页面配置表 ----
|
||
# 每个页面的目录映射、C/C++ 判定覆盖、特殊修正。键=pageId。
|
||
# 目录采用「深缩写」:part 缩写 pNN(顶层不编号)/ 章缩写 chNN / 节缩写 sNN。
|
||
# 例:part1_cpp/ch04_class_object/03_ctor_dtor → p01/ch04/s03
|
||
# 例:part3_qt_desktop/ch04_signal_slot → p03/ch04
|
||
# 单章多节的页:dir 含 sNN 子目录;单章无子节的页:dir 仅到 chNN。
|
||
PAGE_CONFIG = {
|
||
# ---- Part 1: C++ 强化 ---- (p01)
|
||
"58954438": {"part": "p01", "dir": "ch02", "prefix": "p1c02"},
|
||
"58954439": {"part": "p01", "dir": "ch03", "prefix": "p1c03"},
|
||
"58954448": {"part": "p01", "dir": "ch04/s01", "prefix": "p1c04_01"},
|
||
"58954449": {"part": "p01", "dir": "ch04/s02", "prefix": "p1c04_02"},
|
||
"58954451": {"part": "p01", "dir": "ch04/s03", "prefix": "p1c04_03"},
|
||
"58954456": {"part": "p01", "dir": "ch04/s04", "prefix": "p1c04_04"},
|
||
"58954457": {"part": "p01", "dir": "ch04/s05", "prefix": "p1c04_05"},
|
||
"58954458": {"part": "p01", "dir": "ch04/s06", "prefix": "p1c04_06"},
|
||
"58954461": {"part": "p01", "dir": "ch04/s07", "prefix": "p1c04_07"},
|
||
"58954469": {"part": "p01", "dir": "ch04/s08", "prefix": "p1c04_08"},
|
||
"58954472": {"part": "p01", "dir": "ch05", "prefix": "p1c05"},
|
||
"58954473": {"part": "p01", "dir": "ch06", "prefix": "p1c06"},
|
||
"58954474": {"part": "p01", "dir": "ch07", "prefix": "p1c07"},
|
||
"58954476": {"part": "p01", "dir": "ch08", "prefix": "p1c08"},
|
||
"58954487": {"part": "p01", "dir": "ch09/s02", "prefix": "p1c09_02"},
|
||
"58954488": {"part": "p01", "dir": "ch09/s03", "prefix": "p1c09_03"},
|
||
"58954498": {"part": "p01", "dir": "ch09/s04", "prefix": "p1c09_04"},
|
||
# ---- Part 3: Qt 桌面 ---- (p03)
|
||
"58954515": {"part": "p03", "dir": "ch02", "prefix": "p3c02"},
|
||
"58954525": {"part": "p03", "dir": "ch03", "prefix": "p3c03"},
|
||
"58954527": {"part": "p03", "dir": "ch04", "prefix": "p3c04"},
|
||
"58954529": {"part": "p03", "dir": "ch05", "prefix": "p3c05"},
|
||
"58954535": {"part": "p03", "dir": "ch06", "prefix": "p3c06"},
|
||
"58954540": {"part": "p03", "dir": "ch08", "prefix": "p3c08"},
|
||
"58954546": {"part": "p03", "dir": "ch09", "prefix": "p3c09"},
|
||
"58954548": {"part": "p03", "dir": "ch10", "prefix": "p3c10"},
|
||
"58954552": {"part": "p03", "dir": "ch11", "prefix": "p3c11"},
|
||
}
|
||
|
||
# Per-block language overrides (after manual review of ambiguous cases).
|
||
LANG_OVERRIDE = {
|
||
("58954439", 13): "c", # tentative def + printf + EXIT_SUCCESS
|
||
("58954439", 14): "c", # K&R implicit-int (demo of removed C89 feature)
|
||
("58954439", 15): "c", # enum=int + malloc w/o cast (C-only)
|
||
("58954439", 18): "c", # ternary rvalue (C side)
|
||
("58954439", 21): "c", # const modified via cast pointer
|
||
("58954439", 44): "c", # extern "C" guarded header (valid C)
|
||
("58954439", 45): "c", # C impl file
|
||
("58954448", 0): "c", # C structs + strcpy + Person* to Animal*
|
||
}
|
||
|
||
if __name__ == "__main__":
|
||
print("gen_sources: this is the engine module; run gen_part1.py / gen_part3.py.")
|