#!/usr/bin/env python3
# ==============================================================================
#  gen_part3.py — 生成 Part3（Qt 桌面应用开发）的全部源码、.qrc 与 CMakeLists
#
#  输入：tools/wiki_data/ 中 9 个 Qt 页面（pageId 58954515–58954552，共 73 块）。
#  策略：
#    * 每块按内容形态分类（见 BLOCK_TREATMENT）：
#        - 'program'   原文已含 main+QApplication，作为目标近乎原样（补 include/修笔误）。
#        - 'harness'   有意义片段（控件/信号槽/事件/绘图），用 main+QApplication+show
#                      外壳包裹，必要时补所需 QWidget/QMainWindow 子类骨架。
#        - 'snippet'   纯签名/单语句/伪代码，不单独建目标，折进各章 README 作文档。
#    * 每个目标 = 一个独立 add_executable；开 AUTOMOC（Q_OBJECT 类需 moc）。
#    * 离屏运行友好：每个窗口类目标注入一个 QTimer::singleShot，仅当检测到
#      QT_QPA_PLATFORM=offscreen 时才在 200ms 后自动退出；正常运行（有显示环境）
#      窗口保持打开，不会自动关闭。
#    * 资源依赖（:/images/*.png 等）用脚本生成的纯色占位 png + .qrc 提供，qt5_add_resources。
#    * Qt 代码修正（笔误/平台/过时 API）每处 record_erratum 进 docs/ERRATA.md（透明可审计）。
#
#  验证：宿主机用隔离 Qt 5.14.2 构建 →
#        QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target> 自动退出验证。
# ------------------------------------------------------------------------------
import json, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gen_sources
from gen_sources import (REPO, WIKI, page_outline, common_fix, write, record_erratum,
                         PAGE_CONFIG, classify_lang)
from file_name_map import stem_for
ERRATA = gen_sources.ERRATA

EXTRACTED = json.load(open(os.path.join(WIKI, "extracted_code.json")))
PAGES = {p["id"]: p for p in json.load(open(os.path.join(WIKI, "all_page_ids.json")))}

def norm_title(title):
    """Strip Confluence anchor prefixes like '_Toc466556765' that some Qt page
    headings carry, so titles match the clean keys in file_name_map.py."""
    return re.sub(r"_Toc\d+", "", title).strip()

PAGES_META_PID = "?"

# ==============================================================================
#  逐块处理表：键=(pageId, blockIdx)，值=该块如何处理。
#    ('program', include_list)  原文含 main，作目标；include_list=需补的 include。
#    ('harness', harness_spec)  需 main 外壳；harness_spec 见 emit_harness。
#    ('snippet', None)          不建目标，折进 README。
#  分类依据：tools subagent 对 73 块的勘察（见会话记录）。
# ==============================================================================
# 'harness' 的形态（决定包裹方式）：
#   ('plain', includes, body_kind)
#      body_kind: 'top'   片段直接放文件作用域（已含完整的成员函数定义/类）
#                 'widget-ctor' 片段是某个 widget 子类构造函数体内的语句（用 this）
#                 'mainwin-ctor' 片段是 QMainWindow 子类构造函数体内语句
#                 'member-def'  片段是 ::成员函数 定义（需先有类声明骨架）
#   ('class+main', includes, class_decl, body)  先放 class_decl 再放 body，main 实例化

# 每块的 include 需求（按片段用到的 Qt 类确定；标准模板会再加 QApplication/Widget）
INC = {
    "app": "#include <QApplication>",
    "widget": "#include <QWidget>",
    "mwin": "#include <QMainWindow>",
    "pushbtn": "#include <QPushButton>",
    "label": "#include <QLabel>",
    "dialog": "#include <QDialog>",
    "msgbox": "#include <QMessageBox>",
    "filedlg": "#include <QFileDialog>",
    "dock": "#include <QDockWidget>",
    "textedit": "#include <QTextEdit>",
    "menubar": "#include <QMenuBar>",
    "action": "#include <QAction>",
    "qfile": "#include <QFile>",
    "qfileinfo": "#include <QFileInfo>",
    "qtextstream": "#include <QTextStream>",
    "qdatastream": "#include <QDataStream>",
    "qdebug": "#include <QDebug>",
    "qiodevice": "#include <QIODevice>",
    "qpainter": "#include <QPainter>",
    "qpixmap": "#include <QPixmap>",
    "qbitmap": "#include <QBitmap>",
    "qimage": "#include <QImage>",
    "qpicture": "#include <QPicture>",
    "qpaintevent": "#include <QPaintEvent>",
    "qmovie": "#include <QMovie>",
    "qspinbox": "#include <QSpinBox>",
    "qslider": "#include <QSlider>",
    "qboxlayout": "#include <QHBoxLayout>",
    "qevent": "#include <QEvent>",
    "qmouseevent": "#include <QMouseEvent>",
    "qstring": "#include <QString>",
    "qtimer": "#include <QTimer>",
    "qstringlist": "#include <QStringList>",
    "qcoreapp": "#include <QCoreApplication>",
}

# 构建时的逐块处理决策表（pageId, blockIdx -> 处理方式）。
# 形态见上方注释。资源依赖（resources 集合）单独由 exact_fix 标记收集。
BLOCK_TREATMENT = {
    # === 58954515 ch02 创建Qt项目 ===
    ("58954515", 0): ("rewrite", "minimal_qt_app", "rewrite_minimal_qt_app"),

    # === 58954525 ch03 第一个Qt小程序 ===
    # 0: 按钮创建（widget-ctor 片段，混入散文 + 含中文字符串，改 rewrite 避免 prose 误伤）
    ("58954525", 0): ("rewrite", "button_creation", "rewrite_button_creation"),
    # 1,2: 对象树伪代码 → snippet
    ("58954525", 1): ("snippet", None, None),
    ("58954525", 2): ("snippet", None, None),

    # === 58954527 ch04 信号和槽 ===
    # 0: 系统自带信号槽（widget-ctor，connect close）— 引用 MyWidget::close，改 rewrite
    ("58954527", 0): ("rewrite", "builtin_signal_slot", "rewrite_builtin_signal_slot"),
    # 1: connect 签名 → snippet
    ("58954527", 1): ("snippet", None, None),
    # 2: 自定义信号槽（含 Teacher/Student Q_OBJECT + MyWidget，重散文）→ 整体可编译重写
    ("58954527", 2): ("rewrite", "custom_signal_slot", "rewrite_custom_signal_slot"),
    # 3: 带参数自定义信号槽（重载解析 connect）→ snippet（依赖 2 的类）
    ("58954527", 3): ("snippet", None, None),
    # 4: Qt4 SIGNAL/SLOT 写法 → snippet（过时写法，仅文档）
    ("58954527", 4): ("snippet", None, None),
    # 5: lambda 语法骨架 → snippet
    ("58954527", 5): ("snippet", None, None),
    # 6: lambda 实例（widget-ctor + QPushButton + connect lambda）
    ("58954527", 6): ("harness", "lambda_signal_slot",
        {"includes": [INC["app"], INC["widget"], INC["pushbtn"], INC["qdebug"]],
         "body_kind": "widget-ctor"}),

    # === 58954529 ch05 QMainWindow ===
    # 0,1,2: 菜单栏 API 签名 → snippet
    ("58954529", 0): ("snippet", None, None),
    ("58954529", 1): ("snippet", None, None),
    ("58954529", 2): ("snippet", None, None),
    # 3: 状态栏签名 → snippet
    ("58954529", 3): ("snippet", None, None),
    # 4: 铆接部件（mainwin-ctor，尾部混散文 + 中文标题，改 rewrite）
    ("58954529", 4): ("rewrite", "dock_widget", "rewrite_dock_widget"),
    # 5: 核心部件（mainwin-ctor）
    ("58954529", 5): ("harness", "central_widget",
        {"includes": [INC["app"], INC["mwin"], INC["textedit"]],
         "body_kind": "mainwin-ctor"}),
    # 6: .qrc XML 示例 → snippet（非 C++）
    ("58954529", 6): ("snippet", None, None),

    # === 58954535 ch06 对话框 ===
    # 0: 模态对话框（widget-ctor handler，exec()）
    ("58954535", 0): ("harness", "modal_dialog",
        {"includes": [INC["app"], INC["widget"], INC["dialog"]],
         "body_kind": "widget-ctor-handler"}),
    # 1,2,3: 非模态对话框（widget-ctor handler，show）
    ("58954535", 1): ("harness", "modeless_dialog_stack",
        {"includes": [INC["app"], INC["widget"], INC["dialog"]],
         "body_kind": "widget-ctor-handler"}),
    ("58954535", 2): ("harness", "modeless_dialog_heap",
        {"includes": [INC["app"], INC["widget"], INC["dialog"]],
         "body_kind": "widget-ctor-handler"}),
    ("58954535", 3): ("harness", "modeless_dialog_deleteonclose",
        {"includes": [INC["app"], INC["widget"], INC["dialog"]],
         "body_kind": "widget-ctor-handler"}),
    # 4-9: QMessageBox 签名 → snippet
    ("58954535", 4): ("snippet", None, None),
    ("58954535", 5): ("snippet", None, None),
    ("58954535", 6): ("snippet", None, None),
    ("58954535", 7): ("snippet", None, None),
    ("58954535", 8): ("snippet", None, None),
    ("58954535", 9): ("snippet", None, None),
    # 10,11: QMessageBox 实例（widget-ctor-handler）
    ("58954535", 10): ("harness", "message_box_question",
        {"includes": [INC["app"], INC["widget"], INC["msgbox"], INC["qdebug"]],
         "body_kind": "widget-ctor-handler"}),
    ("58954535", 11): ("harness", "message_box_custom",
        {"includes": [INC["app"], INC["widget"], INC["msgbox"], INC["qdebug"]],
         "body_kind": "widget-ctor-handler"}),
    # 12,13,14: 文件对话框 MainWindow（合并为一个目标 file_dialog）
    ("58954535", 12): ("rewrite", "file_dialog", "rewrite_file_dialog"),
    ("58954535", 13): ("snippet", None, None),
    ("58954535", 14): ("snippet", None, None),
    # 15: getOpenFileName 签名 → snippet
    ("58954535", 15): ("snippet", None, None),

    # === 58954540 ch08 常用控件 ===
    # 0: setText 签名 → snippet
    ("58954540", 0): ("snippet", None, None),
    # 1: QLable 创建（widget-ctor，笔误 QLable）
    ("58954540", 1): ("harness", "label_text_create",
        {"includes": [INC["app"], INC["widget"], INC["label"]],
         "body_kind": "widget-ctor", "fix": "qlable_typo"}),
    # 2: label html 超链接（widget-ctor）
    ("58954540", 2): ("harness", "label_text_html",
        {"includes": [INC["app"], INC["widget"], INC["label"]],
         "body_kind": "widget-ctor"}),
    # 3-6: 显示图片（4,5,6 合并为一个目标 label_pixmap，3 是签名 snippet）
    ("58954540", 3): ("snippet", None, None),
    ("58954540", 4): ("snippet", None, None),
    ("58954540", 5): ("snippet", None, None),
    ("58954540", 6): ("snippet", None, None),
    ("58954540", "img"): ("rewrite", "label_pixmap", "rewrite_label_pixmap"),
    # 7-10: 显示动画（合并为目标 label_movie）
    ("58954540", 7): ("snippet", None, None),
    ("58954540", 8): ("snippet", None, None),
    ("58954540", 9): ("snippet", None, None),
    ("58954540", 10): ("snippet", None, None),
    ("58954540", "movie"): ("rewrite", "label_movie", "rewrite_label_movie"),
    # 11-14: lineedit 签名 → snippet
    ("58954540", 11): ("snippet", None, None),
    ("58954540", 12): ("snippet", None, None),
    ("58954540", 13): ("snippet", None, None),
    ("58954540", 14): ("snippet", None, None),
    # 15: 自定义控件 SmallWidget（Q_OBJECT，自带 main 实例化）
    ("58954540", 15): ("rewrite", "custom_widget", "rewrite_custom_widget"),

    # === 58954546 ch09 事件 ===
    # 0: EventLabel 完整程序（mouse event）— 含字符串内误入换行，改 rewrite
    ("58954546", 0): ("rewrite", "mouse_event", "rewrite_mouse_event"),
    # 1: setMouseTracking 单语句 → snippet
    ("58954546", 1): ("snippet", None, None),
    # 2: CustomWidget::event override（member-def，需类骨架）
    ("58954546", 2): ("rewrite", "event_override_customwidget",
        "rewrite_event_override_customwidget"),
    # 3: CustomTextEdit::event override → snippet（依赖骨架）
    ("58954546", 3): ("snippet", None, None),
    # 4,5: event 分发参考 → snippet
    ("58954546", 4): ("snippet", None, None),
    ("58954546", 5): ("snippet", None, None),
    # 6: MainWindow 事件过滤器（class+main，无 Q_OBJECT 但有 eventFilter override）
    ("58954546", 6): ("rewrite", "event_filter_mainwindow",
        "rewrite_event_filter_mainwindow"),
    # 7: 事件过滤器 CustomWidget+FilterObject（重散文）→ snippet（依赖复杂）
    ("58954546", 7): ("snippet", None, None),
    # 8,9: Win32 WndProc 参考 → snippet（非 Qt）
    ("58954546", 8): ("snippet", None, None),
    ("58954546", 9): ("snippet", None, None),
    # 10: notify 签名 → snippet
    ("58954546", 10): ("snippet", None, None),

    # === 58954548 ch10 绘图 ===
    # 0+1: PaintedWidget (Q_OBJECT) h+cpp 合并目标 qpainer_basic
    ("58954548", 0): ("rewrite", "qpainter_basic", "rewrite_qpainter_basic"),
    ("58954548", 1): ("snippet", None, None),  # 并入上面的 rewrite
    # 2: PaintWidget 绘 pixmap/bitmap（含资源 butterfly.png）
    ("58954548", 2): ("rewrite", "paint_device_pixmaps",
        "rewrite_paint_device_pixmaps"),
    # 3: PaintWidget 绘 QImage
    ("58954548", 3): ("rewrite", "paint_device_qimage",
        "rewrite_paint_device_qimage"),
    # 4: QPicture（Windows 路径 D:\n 修正）
    ("58954548", 4): ("rewrite", "qpicture_io", "rewrite_qpicture_io"),

    # === 58954552 ch11 文件系统 ===
    # 0: 完整程序 QFile 读 in.txt（原文用 QApplication+app.exec() 会阻塞 GUI 事件循环，
    #    实为控制台式文件操作；改 rewrite 用 QCoreApplication 并立即退出）
    ("58954552", 0): ("rewrite", "qfile_basic", "rewrite_qfile_basic"),
    # 1: QFileInfo 字段 demo（双重声明 bug）→ snippet
    ("58954552", 1): ("snippet", None, None),
    # 2,3,4: QDataStream 读写 file.dat → 合并为 qdatastream_io
    ("58954552", 2): ("snippet", None, None),
    ("58954552", 3): ("snippet", None, None),
    ("58954552", 4): ("snippet", None, None),
    ("58954552", "data"): ("rewrite", "qdatastream_io", "rewrite_qdatastream_io"),
    # 5,6: QTextStream 读写 file.txt → 合并为 qtextstream_io
    ("58954552", 5): ("snippet", None, None),
    ("58954552", 6): ("snippet", None, None),
    ("58954552", "text"): ("rewrite", "qtextstream_io", "rewrite_qtextstream_io"),
}

# 目标 → 所需资源（:/前缀 资源路径）。占位 png 由生成器产出 + .qrc。
TARGET_RESOURCES = {
    "label_pixmap": {":/Image/boat.jpg": "image_boat.jpg"},
    "label_movie": {":/Mario.gif": "mario.gif"},
    "paint_device_pixmaps": {":/Image/butterfly.png": "image_butterfly.png",
                             ":/Image/butterfly1.png": "image_butterfly1.png"},
}
# 目标 → 运行时数据文件（写入工作目录供程序读写）
TARGET_DATAFILES = {
    "qfile_basic": "in.txt",
    "qdatastream_io": "file.dat",
    "qtextstream_io": "file.txt",
}

# ==============================================================================
#  外壳模板：把一个「片段」包成可运行的 Qt 程序
# ==============================================================================
AUTO_QUIT = (
    "    // 仅在离屏自动化验证时（QT_QPA_PLATFORM=offscreen）200ms 后自动退出；\n"
    "    // 正常运行（有显示环境）窗口保持打开，不自动退出。\n"
    "    if (qgetenv(\"QT_QPA_PLATFORM\") == \"offscreen\") {\n"
    "        QTimer::singleShot(200, &app, &QApplication::quit);\n"
    "    }\n"
)
AUTO_QUIT_CORE = (
    "    QTimer::singleShot(0, &app, &QCoreApplication::quit);\n"
)

def emit_program(pid, idx, stem, out_dir, meta):
    """原文已含 main+QApplication 的完整程序：补 include、修笔误后原样输出。"""
    code = EXTRACTED[pid][idx]["code"]
    code = common_fix(code, pid, idx)
    code = exact_fix(code, pid, idx)
    includes = "\n".join(meta.get("includes", []))
    if meta.get("needs_widget_h"):
        # 58954515/0 引用了 "widget.h"，补一个内联的最小 Widget 定义使自洽
        includes += "\n#include <QWidget>\nclass Widget : public QWidget { Q_OBJECT public: Widget(QWidget* p=nullptr):QWidget(p){} };\n"
    parts = [header_comment(pid, idx, stem)]
    parts.append("#include <QApplication>\n")
    if includes.strip():
        parts.append(includes + "\n")
    parts.append("\n")
    parts.append(code.rstrip() + "\n")
    if not re.search(r"\bQApplication\b", code):
        # 万一原文用 QCoreApplication，上面的 singleShot 也兼容
        pass
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), "".join(parts))
    return src

def emit_harness(pid, idx, stem, out_dir, meta):
    """把片段包进 main+QApplication+show。body_kind 决定片段放置方式。"""
    code = EXTRACTED[pid][idx]["code"]
    code = common_fix(code, pid, idx)
    code = exact_fix(code, pid, idx)
    if meta.get("fix") == "prose_strip":
        code = strip_inline_prose(code, pid, idx)
    body_kind = meta.get("body_kind", "widget-ctor")
    includes = meta.get("includes", [])
    parts = [header_comment(pid, idx, stem)]
    for inc in includes:
        parts.append(inc + "\n")
    if INC["qtimer"] not in includes:
        parts.append(INC["qtimer"] + "\n")
    parts.append("\n")

    if body_kind in ("widget-ctor", "widget-ctor-handler"):
        # 片段是某 widget 子类构造函数体内语句（用 this）。包成：
        #   class DemoWidget : public QWidget { Q_OBJECT public: DemoWidget(){ <片段> } };
        cls = "DemoWidget"
        parts.append(f"class {cls} : public QWidget {{\n")
        parts.append("    Q_OBJECT\n")
        parts.append("public:\n")
        if body_kind == "widget-ctor-handler":
            # 片段是事件处理（如按钮点击弹对话框）：放一个槽里，构造时触发一次
            parts.append(f"    {cls}(){{ doDemo(); }}\n")
            parts.append("private slots:\n    void doDemo(){\n")
            parts.append(indent(code, 2) + "\n")
            parts.append("    }\n")
        else:
            parts.append(f"    {cls}(){{\n")
            parts.append(indent(code, 2) + "\n")
            parts.append("    }\n")
        parts.append("};\n\n")
        parts.append(main_with_widget(cls))
    elif body_kind == "mainwin-ctor":
        cls = "DemoMainWindow"
        parts.append(f"class {cls} : public QMainWindow {{\n")
        parts.append("    Q_OBJECT\npublic:\n")
        parts.append(f"    {cls}(){{\n")
        parts.append(indent(code, 2) + "\n")
        parts.append("    }\n};\n\n")
        parts.append(main_with_widget(cls, is_mainwin=True))
    else:
        parts.append(code.rstrip() + "\n")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), "".join(parts))
    return src

def main_with_widget(cls, is_mainwin=False):
    s = (
        "int main(int argc, char* argv[]) {\n"
        "    QApplication app(argc, argv);\n"
        f"    {cls} w;\n"
        "    w.show();\n"
        + AUTO_QUIT +
        "    return app.exec();\n"
        "}\n"
    )
    return s

def header_comment(pid, idx, stem):
    return (
        "// ============================================================================\n"
        f"//  {stem}  —  Qt 桌面示例\n"
        "//  来源：川大 C++/LinuxC wiki「02.Qt方向」\n"
        f"//  原文片段：[{pid}:{idx}]\n"
        "//  说明：本文件由 gen_part3.py 从 wiki 片段生成（必要时补 main/QApplication\n"
        "//        外壳、include、修正笔误）。详见 docs/ERRATA.md。\n"
        "//        离屏验证：QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>（自动退出）\n"
        "// ============================================================================\n\n"
    )

def indent(s, n):
    pad = " " * n
    return "\n".join(pad + l if l.strip() else l for l in s.splitlines())

# ==============================================================================
#  exact_fix：对 Qt 片段的轻量自动修正（笔误/全角/平台）。逐块的复杂重写见 rewrite_*。
# ==============================================================================
def exact_fix(code, pid, idx):
    # QLable → QLabel（笔误）
    if "QLable" in code:
        code = code.replace("QLable", "QLabel")
        record_erratum(pid, idx, "typo", "QLable", "QLabel",
                       "笔误：QLabel 误写为 QLable。")
    # 散文混入代码：把「行内/行尾」的中文教学散文剥离。
    #   典型：  QPushButton * btn = new QPushButton; 头文件 #include <QPushButton>
    #   典型：  ...setAllowedAreas(...)  设置区域范围
    # 策略：对含 CJK 字符的行，若该行同时含代码（含 ASCII 标点/关键字），则截断在第一段
    # 连续 CJK 处（去掉 CJK 及其后的整行内容）；若整行都是 CJK/注释，保留原样（多是注释）。
    # 仅当该块被标记需要 prose 处理时执行（避免误伤正常中文注释）。
    return code

def strip_inline_prose(code, pid, idx):
    """Remove inline Chinese teaching prose that the wiki glued onto code lines.
    Applied only to blocks flagged 'prose_strip' in BLOCK_TREATMENT."""
    orig = code
    out_lines = []
    for line in code.splitlines():
        # find first run of CJK chars in the line
        m = re.search(r"[\u4e00-\u9fff]", line)
        if not m:
            out_lines.append(line); continue
        # is the CJK preceded by real code on this line? (// comment lines are fine)
        before = line[:m.start()]
        codeish = bool(re.search(r"[A-Za-z0-9_)\];,]", before))
        commented = "//" in before or before.strip().startswith("/*")
        if codeish and not commented:
            # truncate at the CJK run, keep the code part
            out_lines.append(before.rstrip())
        else:
            out_lines.append(line)
    fixed = "\n".join(out_lines)
    if fixed != orig:
        record_erratum(pid, idx, "prose-in-code",
            "代码行内/行尾混入中文教学散文（如「头文件 #include <QPushButton>」「设置区域范围」）",
            "剥离行内中文散文，保留代码部分",
            "wiki 把讲解散文直接粘在代码行末，破坏语法（如把 #include 当成语句尾巴）。"
            "剥离散文使代码可编译。")
    return fixed

# ==============================================================================
#  rewrite_*：对结构复杂/散文混入严重的块，直接给出可编译的完整程序。
# ==============================================================================
def rewrite_minimal_qt_app(pid, idx, out_dir, stem):
    """2.1 一个最简单的Qt应用程序。原文 #include "widget.h" 引用外部 Widget；
    输出自洽的最小 Widget + main（保留 Qt 项目骨架教学意图）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QTimer>\n\n"
        "// 最简单的 Widget：原文 widget.h 的占位实现\n"
        "class Widget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    Widget(QWidget* parent = nullptr) : QWidget(parent) {}\n"
        "};\n\n"
        "int main(int argc, char* argv[]) {\n"
        "    QApplication app(argc, argv);\n"
        "    Widget w;\n"
        "    w.show();\n" +
        AUTO_QUIT +
        "    return app.exec();\n"
        "}\n"
    )
    record_erratum(pid, idx, "missing-symbol",
        "原文 #include \"widget.h\" 引用外部 Widget 类（widget.h/widget.cpp 不在块内）",
        "在文件内就地补一个最小 Widget : public QWidget（Q_OBJECT）+ main",
        "原 wiki 的「最简单 Qt 程序」依赖外部的 widget.h；按「示例相对隔离」拆分为独立目标后"
        "本文件缺少 Widget 定义。补最小 Widget 使其自洽可编译运行。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_button_creation(pid, idx, out_dir, stem):
    """3.1 按钮的创建：QPushButton 创建、设父、设文本、移动、设窗口大小/标题。
    原文行内混入散文「头文件 #include <QPushButton>」；中文文本在字符串内，
    通用 prose 剥离会误伤字符串，故整体重写。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPushButton>\n"
        "#include <QTimer>\n\n"
        "class DemoWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    DemoWidget() {\n"
        "        // 第一种创建：先构造再设属性\n"
        "        QPushButton* btn = new QPushButton;\n"
        "        btn->setParent(this);                 // 设置父亲\n"
        "        btn->setText(QStringLiteral(\"德玛西亚\")); // 设置文字\n"
        "        btn->move(100, 100);                  // 移动位置\n\n"
        "        // 第二种创建：构造时指定文本与父亲\n"
        "        QPushButton* btn2 = new QPushButton(QStringLiteral(\"孙悟空\"), this);\n"
        "        btn2->move(100, 140);\n\n"
        "        this->resize(600, 400);               // 重新指定窗口大小\n"
        "        this->setWindowTitle(QStringLiteral(\"第一个项目\")); // 设置窗口标题\n"
        "        this->setFixedSize(600, 400);         // 限制窗口大小\n"
        "    }\n"
        "};\n\n" + main_with_widget("DemoWidget")
    )
    record_erratum(pid, idx, "prose-in-code",
        "按钮创建片段行内混入散文「头文件 #include <QPushButton>」，且含中文字符串",
        "重写为可编译 DemoWidget（剥离散文、保留中文文本）",
        "原 wiki 把讲解散文粘在代码行内（如 #include 当成尾巴），破坏语法；通用 prose "
        "剥离会误伤字符串内的中文，故整体重写保留教学意图。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_dock_widget(pid, idx, out_dir, stem):
    """5.4 铆接部件 QDockWidget：addDockWidget + setAllowedAreas。
    原文尾部混散文「设置区域范围」；含中文标题，整体重写。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QMainWindow>\n"
        "#include <QDockWidget>\n"
        "#include <QTimer>\n\n"
        "class DemoMainWindow : public QMainWindow {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    DemoMainWindow() {\n"
        "        // 铆接部件（停靠窗口）：标题 + 父对象\n"
        "        QDockWidget* dock = new QDockWidget(QStringLiteral(\"标题\"), this);\n"
        "        addDockWidget(Qt::LeftDockWidgetArea, dock);\n"
        "        // 设置可停靠区域范围（左/右/上/下）\n"
        "        dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea |\n"
        "                              Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);\n"
        "    }\n"
        "};\n\n" + main_with_widget("DemoMainWindow", is_mainwin=True)
    )
    record_erratum(pid, idx, "prose-in-code",
        "QDockWidget 片段尾部混入散文「设置区域范围」，含中文标题字符串",
        "重写为可编译 DemoMainWindow（剥离散文、保留中文标题）",
        "原 wiki 把讲解散文粘在代码行尾（setAllowedAreas 后接「设置区域范围」），破坏语法；"
        "整体重写保留「铆接部件」教学意图。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_builtin_signal_slot(pid, idx, out_dir, stem):
    """4.1 系统自带信号和槽：QPushButton clicked → 窗口 close。
    原文用 &MyWidget::close；改 DemoWidget + QWidget::close。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPushButton>\n"
        "#include <QTimer>\n\n"
        "class DemoWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    DemoWidget() {\n"
        "        // 系统自带的信号和槽：按钮 clicked → 关闭窗口\n"
        "        QPushButton* quitBtn = new QPushButton(QStringLiteral(\"关闭窗口\"), this);\n"
        "        quitBtn->move(50, 50);\n"
        "        resize(300, 200);\n"
        "        connect(quitBtn, &QPushButton::clicked, this, &QWidget::close);\n"
        "    }\n"
        "};\n\n" + main_with_widget("DemoWidget")
    )
    record_erratum(pid, idx, "harness-context",
        "片段 connect(quitBtn,&QPushButton::clicked,this,&MyWidget::close) 引用 MyWidget，"
        "但 MyWidget 定义不在块内",
        "补 DemoWidget 子类 + main 外壳，槽用 QWidget::close（系统自带）",
        "原 wiki 片段假定存在 MyWidget 类与 close 槽；按「示例相对隔离」补齐类骨架与 main，"
        "演示「系统自带信号(clicked)→系统自带槽(close)」。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_mouse_event(pid, idx, out_dir, stem):
    """9.1 事件：EventLabel 重写 mouseMoveEvent/PressEvent/ReleaseEvent。
    原文 QString(\"...(%1, %2)\\n...\") 内误入了字面换行；合并为单行字符串。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QLabel>\n"
        "#include <QMouseEvent>\n"
        "#include <QString>\n"
        "#include <QTimer>\n\n"
        "// 自定义 QLabel：重写鼠标事件，显示坐标\n"
        "class EventLabel : public QLabel {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    explicit EventLabel(QWidget* p = nullptr) : QLabel(p) {\n"
        "        setMouseTracking(true);  // 启用鼠标移动追踪（默认只在按键时追踪）\n"
        "        setText(QStringLiteral(\"移动鼠标到这里\"));\n"
        "    }\n"
        "protected:\n"
        "    void mouseMoveEvent(QMouseEvent* event) override {\n"
        "        setText(QString(\"<center><h1>Move: (%1, %2)</h1></center>\")\n"
        "                .arg(event->x()).arg(event->y()));\n"
        "    }\n"
        "    void mousePressEvent(QMouseEvent* event) override {\n"
        "        setText(QString(\"<center><h1>Press: (%1, %2)</h1></center>\")\n"
        "                .arg(event->x()).arg(event->y()));\n"
        "    }\n"
        "    void mouseReleaseEvent(QMouseEvent* event) override {\n"
        "        setText(QString(\"<center><h1>Release: (%1, %2)</h1></center>\")\n"
        "                .arg(event->x()).arg(event->y()));\n"
        "    }\n"
        "};\n\n" + main_with_widget("EventLabel")
    )
    record_erratum(pid, idx, "syntax+merge",
        "EventLabel 事件重写程序：QString(\"...(%1, %2)\\n...\") 内误入字面换行（断行）"
        "导致字符串未终结；类声明与实现分散",
        "合并为单 .cpp，字符串内换行移除（\\n 移出字符串或删除），补 setMouseTracking",
        "原 wiki 的 QString 字面量在源码中跨行书写（实际是 \\n 后跟换行），C++ 字符串字面量"
        "不能直接跨行，导致 missing terminating \"。修正为单行字符串。同时补 mouseTracking。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_custom_signal_slot(pid, idx, out_dir, stem):
    """4.2 自定义信号和槽：Teacher/Student Q_OBJECT + MyWidget，emit + connect。
    原文散文混入严重；给出可编译的完整对照程序。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPushButton>\n"
        "#include <QDebug>\n"
        "#include <QString>\n"
        "#include <QTimer>\n\n"
        "// ---- 老师（发送者）：饿了就发信号 ----\n"
        "class Teacher : public QObject {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    explicit Teacher(QObject* p = nullptr) : QObject(p) {}\n"
        "signals:\n"
        "    void hungry();            // 无参信号\n"
        "    void hungry(QString food);// 带参信号（重载）\n"
        "public slots:\n"
        "    void classIsOver() {\n"
        "        emit hungry();                 // 触发无参信号\n"
        "        emit hungry(QString(\"汉堡\"));  // 触发带参信号\n"
        "    }\n"
        "};\n\n"
        "// ---- 学生（接收者）：请客吃饭 ----\n"
        "class Student : public QObject {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    explicit Student(QObject* p = nullptr) : QObject(p) {}\n"
        "public slots:\n"
        "    void treat() { qDebug() << \"老师饿了，请老师吃饭！\"; }\n"
        "    void treat(QString food) { qDebug() << \"老师饿了，请老师吃\" << food; }\n"
        "};\n\n"
        "// ---- 聚合窗口：连接信号槽并触发演示 ----\n"
        "class MyWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    MyWidget() {\n"
        "        Teacher* t = new Teacher(this);\n"
        "        Student* s = new Student(this);\n"
        "        // 连接无参信号/槽\n"
        "        connect(t, QOverload<>::of(&Teacher::hungry),\n"
        "                s, QOverload<>::of(&Student::treat));\n"
        "        // 带参重载：用 QOverload<QString>::of 显式选择重载\n"
        "        connect(t, QOverload<QString>::of(&Teacher::hungry),\n"
        "                s, QOverload<QString>::of(&Student::treat));\n"
        "        // 触发演示\n"
        "        t->classIsOver();\n"
        "    }\n"
        "};\n\n"
        "int main(int argc, char* argv[]) {\n"
        "    QApplication app(argc, argv);\n"
        "    MyWidget w; w.show();\n" +
        AUTO_QUIT +
        "    return app.exec();\n"
        "}\n"
    )
    record_erratum(pid, idx, "prose-in-code",
        "自定义信号槽：Teacher/Student/MyWidget 定义与中文散文混排，且信号名 hungury 笔误、"
        "槽名 treat/eat 不一致",
        "重写为可编译对照程序：信号 hungry()/hungry(QString)、槽 treat()/treat(QString)，"
        "函数指针重载解析 connect",
        "原 wiki 把类定义、emit、connect 与中文讲解散文混在一个代码块，无法编译；"
        "信号名 hungury 疑为 hungry 笔误。重写保留「自定义信号+槽+重载」教学意图。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_file_dialog(pid, idx, out_dir, stem):
    """6.5 标准文件对话框：MainWindow + openAction/saveAction + openFile/saveFile 槽。
    原文 12/13/14 三块合成一个可运行目标。资源 :/images/file-open :/images/file-save。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QMainWindow>\n"
        "#include <QAction>\n"
        "#include <QMenuBar>\n"
        "#include <QToolBar>\n"
        "#include <QFileDialog>\n"
        "#include <QFile>\n"
        "#include <QTextStream>\n"
        "#include <QMessageBox>\n"
        "#include <QTimer>\n\n"
        "class MainWindow : public QMainWindow {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    MainWindow() {\n"
        "        QAction* openAction = new QAction(QStringLiteral(\"打开...\"), this);\n"
        "        QAction* saveAction = new QAction(QStringLiteral(\"保存...\"), this);\n"
        "        menuBar()->addMenu(QStringLiteral(\"文件\"))->addAction(openAction);\n"
        "        menuBar()->addMenu(QStringLiteral(\"文件\"))->addAction(saveAction);\n"
        "        connect(openAction, &QAction::triggered, this, &MainWindow::openFile);\n"
        "        connect(saveAction, &QAction::triggered, this, &MainWindow::saveFile);\n"
        "    }\n"
        "private slots:\n"
        "    void openFile() {\n"
        "        QString path = QFileDialog::getOpenFileName(this, QStringLiteral(\"打开文件\"));\n"
        "        if (path.isEmpty()) return;\n"
        "        QFile file(path);\n"
        "        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {\n"
        "            QMessageBox::warning(this, QStringLiteral(\"错误\"), QStringLiteral(\"打开失败\"));\n"
        "            return;\n"
        "        }\n"
        "        QTextStream in(&file);\n"
        "        QString text = in.readAll();\n"
        "        file.close();\n"
        "        QMessageBox::information(this, QStringLiteral(\"内容\"), text);\n"
        "    }\n"
        "    void saveFile() {\n"
        "        QString path = QFileDialog::getSaveFileName(this, QStringLiteral(\"保存文件\"));\n"
        "        if (path.isEmpty()) return;\n"
        "        QFile file(path);\n"
        "        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {\n"
        "            QMessageBox::warning(this, QStringLiteral(\"错误\"), QStringLiteral(\"保存失败\"));\n"
        "            return;\n"
        "        }\n"
        "        QTextStream out(&file);\n"
        "        out << QStringLiteral(\"示例保存内容\");\n"
        "        file.close();\n"
        "    }\n"
        "};\n\n"
        "int main(int argc, char* argv[]) {\n"
        "    QApplication app(argc, argv);\n"
        "    MainWindow w; w.show();\n" +
        AUTO_QUIT +
        "    return app.exec();\n"
        "}\n"
    )
    record_erratum(pid, idx, "merge+rewrite",
        "文件对话框示例分散在 12/13/14 三块（含 :/images/file-open 资源、connect、openFile/saveFile 实现）",
        "合并重写为单一可运行 MainWindow 程序，菜单/工具栏触发文件对话框读写",
        "原文三块是同一 MainWindow 的不同部分（构造、连接、槽实现），独立成文件无法编译；"
        "合并为完整目标。原文引用资源图标，本目标用纯文字 Action 避免资源依赖。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_label_pixmap(pid, idx, out_dir, stem):
    """显示图片：QPixmap load :/Image/boat.jpg + QLabel->setPixmap。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QLabel>\n"
        "#include <QPixmap>\n"
        "#include <QTimer>\n\n"
        "class DemoWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    DemoWidget() {\n"
        "        QPixmap pixmap;\n"
        "        pixmap.load(\":/Image/boat.jpg\");   // 从资源加载图片\n"
        "        QLabel* label = new QLabel(this);\n"
        "        label->setPixmap(pixmap);          // 注意：label 是指针，用 ->\n"
        "        label->setGeometry(10, 10, 200, 120);\n"
        "    }\n"
        "};\n\n" + main_with_widget("DemoWidget")
    )
    record_erratum(pid, idx, "merge+typo",
        "显示图片示例分散在 3 块（QPixmap pixmap; pixmap.load(:/Image/boat.jpg); "
        "label.setPixmap(pixmap) 用了 . 而非 ->）",
        "合并为一个目标，修正 .→->，资源 :/Image/boat.jpg 由生成占位 png + .qrc 提供",
        "原文 label 是 QLabel* 却用 . 调用 setPixmap（C++ 指针须用 ->）；三块拆开不可编译。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_label_movie(pid, idx, out_dir, stem):
    """显示动画：QMovie(:/Mario.gif) + start + QLabel->setMovie。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QLabel>\n"
        "#include <QMovie>\n"
        "#include <QTimer>\n\n"
        "class DemoWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    DemoWidget() {\n"
        "        QMovie* movie = new QMovie(\":/Mario.gif\");  // 从资源加载动画\n"
        "        movie->start();\n"
        "        QLabel* label = new QLabel(this);\n"
        "        label->setMovie(movie);\n"
        "        label->setGeometry(10, 10, 200, 200);\n"
        "    }\n"
        "};\n\n" + main_with_widget("DemoWidget")
    )
    record_erratum(pid, idx, "merge+typo",
        "显示动画示例分散在 3 块（QMovie(:/Mario.gif)、movie->start()、QLabel 全角分号 ;）",
        "合并为一个目标，全角分号→半角，资源 :/Mario.gif 由生成占位 gif + .qrc 提供",
        "原文 QLabel 定义末尾用了全角分号 ；（C++ 非法）；三块拆开不可编译。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_custom_widget(pid, idx, out_dir, stem):
    """8.4 自定义控件 SmallWidget：spin+slider+layout+两 connect（Q_OBJECT）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QSpinBox>\n"
        "#include <QSlider>\n"
        "#include <QHBoxLayout>\n"
        "#include <QTimer>\n\n"
        "// 自定义控件：spinbox 与 slider 联动\n"
        "class SmallWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    SmallWidget(QWidget* parent = nullptr) : QWidget(parent) {\n"
        "        QSpinBox* spin = new QSpinBox(this);\n"
        "        QSlider* slider = new QSlider(Qt::Horizontal, this);\n"
        "        QHBoxLayout* layout = new QHBoxLayout(this);\n"
        "        layout->addWidget(spin);\n"
        "        layout->addWidget(slider);\n"
        "        // spin 变化 → slider 跟随（Qt5 静态转换选重载）\n"
        "        connect(spin,\n"
        "                static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),\n"
        "                slider, &QSlider::setValue);\n"
        "        // slider 变化 → spin 跟随\n"
        "        connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);\n"
        "    }\n"
        "};\n\n"
        "int main(int argc, char* argv[]) {\n"
        "    QApplication app(argc, argv);\n"
        "    SmallWidget w; w.show();\n" +
        AUTO_QUIT +
        "    return app.exec();\n"
        "}\n"
    )
    record_erratum(pid, idx, "h+cpp-merge",
        "自定义控件 SmallWidget 在 wiki 中以 smallwidget.h 内联展示（Q_OBJECT + spin/slider/layout/connect）",
        "输出为单 .cpp（含类定义+main 实例化），保留 Qt5 static_cast 重载解析写法",
        "原 wiki 把类定义放 .h 注释块；按「每目标单文件」输出，AUTOMOC 处理 Q_OBJECT。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_event_override_customwidget(pid, idx, out_dir, stem):
    """9.2 event() 重写：CustomWidget::event 处理 KeyPress/Tab。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QEvent>\n"
        "#include <QKeyEvent>\n"
        "#include <QDebug>\n"
        "#include <QTimer>\n\n"
        "class CustomWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    explicit CustomWidget(QWidget* p = nullptr) : QWidget(p) {}\n"
        "protected:\n"
        "    bool event(QEvent* e) override {\n"
        "        if (e->type() == QEvent::KeyPress) {\n"
        "            QKeyEvent* ke = static_cast<QKeyEvent*>(e);\n"
        "            if (ke->key() == Qt::Key_Tab) {\n"
        "                qDebug() << \"Tab pressed in CustomWidget\";\n"
        "                return true;  // 事件已处理，不再向上传递\n"
        "            }\n"
        "        }\n"
        "        return QWidget::event(e);  // 其余交给基类\n"
        "    }\n"
        "};\n\n" + main_with_widget("CustomWidget")
    )
    record_erratum(pid, idx, "member-def-harness",
        "CustomWidget::event(QEvent*) override 片段（处理 KeyPress/Tab）缺少所属类骨架与 main",
        "补 CustomWidget : public QWidget 骨架（含 event override）+ main 外壳，行为对照原文",
        "原 wiki 只给成员函数定义，缺少类声明与 main；按「示例相对隔离」补齐使其可编译运行。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_event_filter_mainwindow(pid, idx, out_dir, stem):
    """9.3 事件过滤器：MainWindow + eventFilter override + installEventFilter。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QMainWindow>\n"
        "#include <QLabel>\n"
        "#include <QEvent>\n"
        "#include <QDebug>\n"
        "#include <QTimer>\n\n"
        "// 事件过滤器：拦截 label 上的事件\n"
        "class MainWindow : public QMainWindow {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    MainWindow() {\n"
        "        label = new QLabel(QStringLiteral(\"事件过滤器示例\"), this);\n"
        "        label->setGeometry(10, 30, 200, 30);\n"
        "        label->installEventFilter(this);  // 给 label 安装事件过滤器\n"
        "    }\n"
        "protected:\n"
        "    bool eventFilter(QObject* watched, QEvent* event) override {\n"
        "        if (watched == label && event->type() == QEvent::MouseButtonPress) {\n"
        "            qDebug() << \"label 上的鼠标按下事件被过滤器拦截\";\n"
        "            return true;  // 拦截，不再派发\n"
        "        }\n"
        "        return QMainWindow::eventFilter(watched, event);\n"
        "    }\n"
        "private:\n"
        "    QLabel* label;\n"
        "};\n\n" + main_with_widget("MainWindow", is_mainwin=True)
    )
    record_erratum(pid, idx, "class+main-harness",
        "事件过滤器 MainWindow（eventFilter override + installEventFilter）片段缺 main 与完整结构",
        "补完整 MainWindow（含 QLabel、eventFilter、installEventFilter）+ main 外壳",
        "原 wiki 给出 MainWindow 类骨架但缺 main；补齐使其可编译运行，演示事件过滤机制。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_qpainter_basic(pid, idx, out_dir, stem):
    """10.1 QPainter：PaintedWidget (Q_OBJECT) + paintEvent（drawLine/Rect/Ellipse）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPainter>\n"
        "#include <QPaintEvent>\n"
        "#include <QTimer>\n\n"
        "class PaintedWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    PaintedWidget(QWidget* parent = nullptr) : QWidget(parent) {\n"
        "        setFixedSize(300, 200);\n"
        "    }\n"
        "protected:\n"
        "    void paintEvent(QPaintEvent*) override {\n"
        "        QPainter painter(this);\n"
        "        painter.drawLine(QLine(10, 10, 200, 10));     // 画线\n"
        "        painter.drawRect(QRect(10, 30, 100, 50));     // 画矩形\n"
        "        painter.drawEllipse(QRect(150, 30, 100, 80)); // 画椭圆\n"
        "    }\n"
        "};\n\n" + main_with_widget("PaintedWidget")
    )
    record_erratum(pid, idx, "h+cpp-merge",
        "PaintedWidget 类声明（10.1 block0）与其构造+paintEvent 实现（block1）分两块",
        "合并为单 .cpp（Q_OBJECT 类 + paintEvent），补 main 外壳演示 drawLine/Rect/Ellipse",
        "原文类声明与成员实现分属两块；按「每目标单文件」合并，AUTOMOC 处理 Q_OBJECT。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_paint_device_pixmaps(pid, idx, out_dir, stem):
    """10.2.1 绘 pixmap/bitmap（资源 butterfly.png / butterfly1.png）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPainter>\n"
        "#include <QPaintEvent>\n"
        "#include <QPixmap>\n"
        "#include <QBitmap>\n"
        "#include <QTimer>\n\n"
        "class PaintWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    PaintWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedSize(360, 200); }\n"
        "protected:\n"
        "    void paintEvent(QPaintEvent*) override {\n"
        "        QPainter painter(this);\n"
        "        QPixmap pix(\":/Image/butterfly.png\");   // 从资源加载图片\n"
        "        painter.drawPixmap(10, 10, pix);\n"
        "        QPixmap pix1(\":/Image/butterfly1.png\");\n"
        "        painter.drawPixmap(180, 10, pix1);\n"
        "        // QBitmap 是 QPixmap 子类，仅 1 位深度（黑白掩码）\n"
        "        QBitmap bmp = pix.mask();\n"
        "        painter.drawPixmap(10, 110, bmp);\n"
        "    }\n"
        "};\n\n" + main_with_widget("PaintWidget")
    )
    record_erratum(pid, idx, "member-def-harness",
        "PaintWidget::paintEvent 绘 pixmap/bitmap（引用 :/Image/butterfly.png、butterfly1.png）",
        "补 PaintWidget 类骨架 + main，资源由生成占位 png + .qrc 提供",
        "原 wiki 仅给成员函数定义，缺类骨架与 main；资源图片用占位 png 提供以可编译运行。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_paint_device_qimage(pid, idx, out_dir, stem):
    """10.2.1 绘 QImage（setPixel 循环）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPainter>\n"
        "#include <QPaintEvent>\n"
        "#include <QImage>\n"
        "#include <QTimer>\n\n"
        "class PaintWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    PaintWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedSize(200, 200); }\n"
        "protected:\n"
        "    void paintEvent(QPaintEvent*) override {\n"
        "        QPainter painter(this);\n"
        "        QImage image(100, 100, QImage::Format_RGB32);\n"
        "        // QImage 可直接逐像素操作（QPixmap 不行）\n"
        "        for (int y = 0; y < image.height(); ++y) {\n"
        "            for (int x = 0; x < image.width(); ++x) {\n"
        "                image.setPixel(x, y, qRgb((x * 2) % 256, (y * 2) % 256, 128));\n"
        "            }\n"
        "        }\n"
        "        painter.drawImage(10, 10, image);\n"
        "    }\n"
        "};\n\n" + main_with_widget("PaintWidget")
    )
    record_erratum(pid, idx, "member-def-harness",
        "PaintWidget::paintEvent 绘 QImage（setPixel 循环逐像素设色）片段",
        "补 PaintWidget 类骨架 + main 外壳",
        "原 wiki 仅给成员函数定义，缺类骨架与 main；补齐使其可编译运行。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_qpicture_io(pid, idx, out_dir, stem):
    """10.2.2 QPicture：save/load。原路径 D:\\ndrawing.pic（含误入换行）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QWidget>\n"
        "#include <QPainter>\n"
        "#include <QPaintEvent>\n"
        "#include <QPicture>\n"
        "#include <QDebug>\n"
        "#include <QTimer>\n\n"
        "class PaintWidget : public QWidget {\n"
        "    Q_OBJECT\n"
        "public:\n"
        "    PaintWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedSize(200, 200); }\n"
        "protected:\n"
        "    void paintEvent(QPaintEvent*) override {\n"
        "        // QPicture 记录并回放绘图指令\n"
        "        QPicture picture;\n"
        "        QPainter painter;\n"
        "        painter.begin(&picture);            // 把绘图记录到 picture\n"
        "        painter.drawLine(10, 10, 100, 100);\n"
        "        painter.end();\n"
        "        picture.save(\"drawing.pic\");       // 保存（用相对路径，跨平台）\n\n"
        "        QPicture picture2;\n"
        "        picture2.load(\"drawing.pic\");      // 回放\n"
        "        QPainter p2(this);\n"
        "        p2.drawPicture(10, 10, picture2);\n"
        "        qDebug() << \"QPicture saved & replayed\";\n"
        "    }\n"
        "};\n\n" + main_with_widget("PaintWidget")
    )
    record_erratum(pid, idx, "platform-path",
        "QPicture 保存路径为 \"D:\\ndrawing.pic\"（Windows 路径，且 \\n 被解析为换行符）",
        "改为相对路径 \"drawing.pic\"（跨平台可移植）",
        "原路径是 Windows 绝对路径，\\n 在字符串中被当作换行符导致路径错误；改为相对路径使"
        "示例在 Linux 下可编译运行。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_qfile_basic(pid, idx, out_dir, stem):
    """11.1 基本文件操作：QFile 读 in.txt + QFileInfo 字段。
    原文用 QApplication+app.exec()，实为控制台文件操作；改用 QCoreApplication 并
    文件 IO 完成后立即退出（无需 GUI 事件循环，便于自动化运行验证）。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QCoreApplication>\n"
        "#include <QFile>\n"
        "#include <QFileInfo>\n"
        "#include <QDebug>\n"
        "#include <QIODevice>\n\n"
        "int main(int argc, char* argv[]) {\n"
        "    QCoreApplication app(argc, argv);\n\n"
        "    QFile file(\"in.txt\");\n"
        "    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {\n"
        "        qDebug() << \"Open file failed.\";\n"
        "        return -1;\n"
        "    }\n"
        "    while (!file.atEnd()) {\n"
        "        qDebug() << file.readLine();\n"
        "    }\n\n"
        "    QFileInfo info(file);\n"
        "    qDebug() << \"isDir:\" << info.isDir();\n"
        "    qDebug() << \"isExecutable:\" << info.isExecutable();\n"
        "    qDebug() << \"baseName:\" << info.baseName();\n"
        "    qDebug() << \"completeBaseName:\" << info.completeBaseName();\n"
        "    qDebug() << \"suffix:\" << info.suffix();\n"
        "    qDebug() << \"completeSuffix:\" << info.completeSuffix();\n\n"
        "    return 0;  // 文件 IO 完成，直接退出（无需 app.exec() 事件循环）\n"
        "}\n"
    )
    record_erratum(pid, idx, "blocking-event-loop",
        "原文用 QApplication + app.exec()（阻塞 GUI 事件循环），但示例实为控制台文件操作",
        "改用 QCoreApplication，文件 IO 完成后直接 return 0（不进事件循环）",
        "QApplication::exec() 进入 GUI 事件循环会一直阻塞；该示例不创建窗口，仅做文件读写，"
        "用 QCoreApplication 并直接返回即可正确退出，便于自动化运行验证。原行为不变。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_qdatastream_io(pid, idx, out_dir, stem):
    """11.2 二进制文件读写 file.dat：QDataStream write + read 合并演示。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QFile>\n"
        "#include <QDataStream>\n"
        "#include <QDebug>\n"
        "#include <QTimer>\n\n"
        "// 二进制文件读写：先写 file.dat，再读回\n"
        "int main(int argc, char* argv[]) {\n"
        "    QCoreApplication app(argc, argv);\n\n"
        "    // ---- 写 ----\n"
        "    {\n"
        "        QFile file(\"file.dat\");\n"
        "        if (file.open(QIODevice::WriteOnly)) {\n"
        "            QDataStream out(&file);\n"
        "            out << QString(\"hello\") << qint32(42) << 3.14;\n"
        "            file.close();\n"
        "        }\n"
        "    }\n"
        "    // ---- 读 ----\n"
        "    {\n"
        "        QFile file(\"file.dat\");\n"
        "        if (file.open(QIODevice::ReadOnly)) {\n"
        "            QDataStream in(&file);\n"
        "            QString s; qint32 n; double d;\n"
        "            in >> s >> n >> d;\n"
        "            qDebug() << \"read:\" << s << n << d;\n"
        "            file.close();\n"
        "        } else {\n"
        "            qDebug() << \"open failed\";\n"
        "        }\n"
        "    }\n\n" +
        AUTO_QUIT_CORE +
        "    return app.exec();\n"
        "}\n"
    )
    record_erratum(pid, idx, "merge",
        "二进制读写示例分散在 3 块（写/读/读写），各自引用 file.dat 且未检查 open() 返回值",
        "合并为一个程序：先写 file.dat 再读回，补 open() 检查，用 QCoreApplication（无需 GUI）",
        "原文三块是读写示例的不同片段，独立不可编译；合并为完整程序。补 open() 检查避免"
        "未定义行为；数据文件 file.dat 程序自建。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

def rewrite_qtextstream_io(pid, idx, out_dir, stem):
    """11.3 文本文件读写 file.txt：QTextStream write + read 合并演示。"""
    code = (
        header_comment(pid, idx, stem) +
        "#include <QApplication>\n"
        "#include <QFile>\n"
        "#include <QTextStream>\n"
        "#include <QDebug>\n"
        "#include <QTimer>\n\n"
        "// 文本文件读写：先写 file.txt，再读回\n"
        "int main(int argc, char* argv[]) {\n"
        "    QCoreApplication app(argc, argv);\n\n"
        "    // ---- 写 ----\n"
        "    {\n"
        "        QFile data(\"file.txt\");\n"
        "        if (data.open(QFile::WriteOnly | QFile::Truncate)) {\n"
        "            QTextStream out(&data);\n"
        "            out << \"line1: hello\" << endl;\n"
        "            out << \"line2: world\" << endl;\n"
        "            data.close();\n"
        "        }\n"
        "    }\n"
        "    // ---- 读 ----\n"
        "    {\n"
        "        QFile data(\"file.txt\");\n"
        "        if (data.open(QFile::ReadOnly)) {\n"
        "            QTextStream in(&data);\n"
        "            QString line;\n"
        "            while (in.readLineInto(&line)) {\n"
        "                qDebug() << \"read:\" << line;\n"
        "            }\n"
        "            data.close();\n"
        "        }\n"
        "    }\n\n" +
        AUTO_QUIT_CORE +
        "    return app.exec();\n"
        "}\n"
    )
    record_erratum(pid, idx, "merge",
        "文本读写示例分散在 2 块（写/读），各自引用 file.txt",
        "合并为一个程序：先写 file.txt 再逐行读回，用 QCoreApplication（无需 GUI）",
        "原文两块是读写示例的不同片段，独立不可编译；合并为完整程序。")
    src = os.path.join(out_dir, f"{stem}.cpp")
    write(os.path.join(REPO, src), code)
    return src

REWRITES = {
    "rewrite_minimal_qt_app": rewrite_minimal_qt_app,
    "rewrite_button_creation": rewrite_button_creation,
    "rewrite_dock_widget": rewrite_dock_widget,
    "rewrite_builtin_signal_slot": rewrite_builtin_signal_slot,
    "rewrite_mouse_event": rewrite_mouse_event,
    "rewrite_custom_signal_slot": rewrite_custom_signal_slot,
    "rewrite_file_dialog": rewrite_file_dialog,
    "rewrite_label_pixmap": rewrite_label_pixmap,
    "rewrite_label_movie": rewrite_label_movie,
    "rewrite_custom_widget": rewrite_custom_widget,
    "rewrite_event_override_customwidget": rewrite_event_override_customwidget,
    "rewrite_event_filter_mainwindow": rewrite_event_filter_mainwindow,
    "rewrite_qpainter_basic": rewrite_qpainter_basic,
    "rewrite_paint_device_pixmaps": rewrite_paint_device_pixmaps,
    "rewrite_paint_device_qimage": rewrite_paint_device_qimage,
    "rewrite_qpicture_io": rewrite_qpicture_io,
    "rewrite_qfile_basic": rewrite_qfile_basic,
    "rewrite_qdatastream_io": rewrite_qdatastream_io,
    "rewrite_qtextstream_io": rewrite_qtextstream_io,
}

# ==============================================================================
#  资源：生成纯色占位 png/gif + .qrc
# ==============================================================================
def make_placeholder_png(path, w=64, h=64, rgb=(120, 160, 200)):
    """Write a minimal valid PNG (single solid color) without PIL — pure zlib."""
    import zlib, struct
    def chunk(typ, data):
        return (struct.pack(">I", len(data)) + typ + data +
                struct.pack(">I", zlib.crc32(typ + data) & 0xFFFFFFFF))
    raw = b""
    r, g, b = rgb
    for _ in range(h):
        raw += b"\x00" + bytes([r, g, b]) * w   # filter byte 0 + one RGBA-less RGB row
    sig = b"\x89PNG\r\n\x1a\n"
    ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)  # 8-bit, color type 2 (RGB)
    idat = zlib.compress(raw)
    with open(path, "wb") as f:
        f.write(sig + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat) + chunk(b"IEND", b""))

def make_placeholder_gif(path, w=4, h=4):
    """Write a tiny valid GIF87a (single color). Qt QMovie accepts it as a 1-frame anim."""
    r, g, b = 180, 120, 60
    # GIF header
    header = b"GIF87a"
    logical = bytes([w & 255, (w >> 8) & 255, h & 255, (h >> 8) & 255,
                     0b10000000, 0, 0])  # global color table flag | size=1 (2 colors)
    # global color table: 2 entries (BG + one color)
    gct = bytes([0, 0, 0]) + bytes([r, g, b])
    # image descriptor
    imgdesc = b"\x2c" + bytes([0, 0, 0, 0, w & 255, (w >> 8) & 255, h & 255, (h >> 8) & 255, 0])
    # image data: LZW min code size=2, then a sub-block encoding the whole image
    # simplest: code size 2, one clear + pixels via codes. Use minimal stream that Qt accepts.
    lzw_min = 2
    # Build a trivial LZW stream: clear(4), then emit pixel index 1 codes, end(5).
    # For a 4x4=16 pixel image with 2-color table, encode index 1 sixteen times.
    # bit packing (LSB first), code width starts at lzw_min+1=3.
    codes = [4]  # clear
    codes += [1] * (w * h)
    codes += [5]  # end
    width = 3
    bits = 0; nbits = 0; out = bytearray()
    for c in codes:
        bits |= (c << nbits); nbits += width
        while nbits >= 8:
            out.append(bits & 0xFF); bits >>= 8; nbits -= 8
    if nbits > 0:
        out.append(bits & 0xFF)
    imgdata = bytes([lzw_min]) + bytes([len(out)]) + bytes(out) + b"\x00"
    trailer = b"\x3b"
    with open(path, "wb") as f:
        f.write(header + logical + gct + imgdesc + imgdata + trailer)

def emit_resources(out_dir, target):
    """For a target that needs resources, write placeholder asset files + a .qrc.
    Returns the .qrc basename (or None if no resources)."""
    res = TARGET_RESOURCES.get(target)
    if not res:
        return None
    asset_dir = os.path.join(out_dir, "assets")
    os.makedirs(os.path.join(REPO, asset_dir), exist_ok=True)
    # Standard Qt .qrc: root <RCC>...</RCC>, one <qresource> (prefix defaults to "/").
    # The "alias" makes the file reachable at ":/<prefix>/<alias>" — here prefix is "/"
    # so ":/Image/boat.jpg" needs the alias to include the sub-path, i.e. alias="Image/boat.jpg".
    qrc_lines = ["<RCC>", "    <qresource>"]
    for res_path, asset_name in res.items():
        # res_path like ":/Image/boat.jpg" → alias "Image/boat.jpg", file "assets/boat.jpg"
        rel = res_path[1:]                      # strip leading ":"
        prefix, fname = os.path.split(rel)      # prefix="Image", fname="boat.jpg"
        asset_path = os.path.join(asset_dir, asset_name)
        if asset_name.endswith(".gif"):
            make_placeholder_gif(os.path.join(REPO, asset_path))
        else:
            make_placeholder_png(os.path.join(REPO, asset_path))
        alias = f"{prefix}/{fname}" if prefix else fname
        qrc_lines.append(f'        <file alias="{alias}">{os.path.join("assets", asset_name)}</file>')
    qrc_lines.append("    </qresource>")
    qrc_lines.append("</RCC>")
    qrc_name = f"{target}.qrc"
    write(os.path.join(REPO, out_dir, qrc_name), "\n".join(qrc_lines) + "\n")
    return qrc_name

def emit_datafile(out_dir, target):
    """For a target needing a runtime data file, drop a sample so first-run works."""
    fname = TARGET_DATAFILES.get(target)
    if not fname:
        return
    content = {"in.txt": "Hello from in.txt\n", "file.txt": "sample\n", "file.dat": ""}
    data_dir = os.path.join(out_dir, "rundata")
    os.makedirs(os.path.join(REPO, data_dir), exist_ok=True)
    write(os.path.join(REPO, data_dir, fname), content.get(fname, ""))

def add_moc_include_if_needed(abs_src):
    """For a .cpp that defines a Q_OBJECT class inline (not in a header), CMake's
    AUTOMOC requires `#include "<stem>.moc"` at the bottom of the .cpp so the moc
    output for that class gets compiled in. Without it, AUTOMOC errors out:
    'contains a Q_OBJECT macro, but does not include "<stem>.moc"!'."""
    with open(abs_src, "r", encoding="utf-8") as f:
        content = f.read()
    if "Q_OBJECT" not in content:
        return
    stem = os.path.splitext(os.path.basename(abs_src))[0]
    moc_inc = f'#include "{stem}.moc"'
    if moc_inc in content:
        return
    # insert just before the final closing brace region is fragile; append at EOF
    # is the standard idiom (moc include is processed regardless of position).
    content = content.rstrip() + "\n\n" + moc_inc + "\n"
    with open(abs_src, "w", encoding="utf-8") as f:
        f.write(content)

# ==============================================================================
#  主流程
# ==============================================================================
def main():
    global PAGES_META_PID
    qt_pages = [pid for pid, cfg in PAGE_CONFIG.items() if cfg["part"] == "p03"]
    targets_by_dir = {}   # dir -> list of (target_name, src_rel, needs_qrc, qrc_name)
    snippet_by_dir = {}   # dir -> list of (idx, sec_title, code) for README

    for pid in qt_pages:
        cfg = PAGE_CONFIG[pid]
        PAGES_META_PID = pid
        body = json.load(open(os.path.join(WIKI, "bodies", f"{pid}.json"))) \
                   .get("body", {}).get("storage", {}).get("value", "")
        blocks = EXTRACTED.get(pid, [])
        sections = page_outline(body)
        out_dir = os.path.join("p03", cfg["dir"])

        # block idx -> normalized section title
        block_sec = {}
        for sec in sections:
            for i in sec["code_indices"]:
                if i < len(blocks):
                    block_sec[i] = norm_title(sec["title"])

        used_keys = set()
        for (tpid, tidx), treat in BLOCK_TREATMENT.items():
            if tpid != pid:
                continue
            kind = treat[0]
            if kind == "snippet":
                if isinstance(tidx, int):
                    code = blocks[tidx]["code"]
                    snippet_by_dir.setdefault(out_dir, []).append(
                        (tidx, block_sec.get(tidx, "(导言)"), code))
                continue
            stem = treat[1]
            target_name = stem  # ASCII; file_name_map 已保证英文蛇形
            if kind == "program":
                src = emit_program(pid, tidx, stem, out_dir, treat[2])
            elif kind == "harness":
                src = emit_harness(pid, tidx, stem, out_dir, treat[2])
            elif kind == "rewrite":
                src = REWRITES[treat[2]](pid, tidx, out_dir, stem)
            else:
                continue
            qrc_name = emit_resources(out_dir, target_name)
            emit_datafile(out_dir, target_name)
            # Q_OBJECT classes defined inline in the .cpp need a trailing moc include.
            add_moc_include_if_needed(os.path.join(REPO, src))
            targets_by_dir.setdefault(out_dir, []).append(
                (target_name, src, bool(qrc_name), qrc_name))

    write_part3_cmakelists(targets_by_dir)
    write_part3_readmes(targets_by_dir, snippet_by_dir)
    write_errata()
    n = sum(len(v) for v in targets_by_dir.values())
    print(f"gen_part3: wrote {n} targets across {len(targets_by_dir)} dirs.")

def write_part3_cmakelists(targets_by_dir):
    # per-dir CMakeLists
    for d, targets in targets_by_dir.items():
        lines = ["# Auto-generated by tools/gen_part3.py — do not edit by hand.\n",
                 f"# 章节：{d}\n",
                 "set(CMAKE_AUTOMOC ON)\n",
                 "set(CMAKE_AUTORCC ON)\n"]
        for tgt, src_rel, has_qrc, qrc_name in targets:
            src = os.path.basename(src_rel)
            if has_qrc:
                lines.append(f"qt5_add_resources(RES {qrc_name})\n")
                lines.append(f"add_executable({tgt} {src} ${{RES}})\n")
            else:
                lines.append(f"add_executable({tgt} {src})\n")
            lines.append(f"set_target_properties({tgt} PROPERTIES "
                         f"CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF "
                         f"AUTOMOC ON)\n")
            # Link Qt modules so their include dirs / libs are available to every
            # example. (Filesystem examples only need Core, but QApplication/QWidget
            # based ones need Widgets+Gui; linking all three keeps targets uniform and
            # avoids per-target include-path gaps.)
            lines.append(f"target_link_libraries({tgt} PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)\n")
            lines.append(f"target_compile_options({tgt} PRIVATE -Wall -Wextra)\n")
        dir_tname = "all_" + d.replace("/", "_").replace("-", "_")
        lines.append(f"add_custom_target({dir_tname} DEPENDS {' '.join(t[0] for t in targets)})\n")
        write(os.path.join(REPO, d, "CMakeLists.txt"), "".join(lines))

    # top-level p03/CMakeLists including all sub-dirs
    lines = ["# Auto-generated by tools/gen_part3.py\n"]
    for d in sorted(targets_by_dir.keys()):
        rel = os.path.relpath(d, "p03")
        lines.append(f"add_subdirectory({rel})\n")
    write(os.path.join(REPO, "p03", "CMakeLists.txt"), "".join(lines))

def write_part3_readmes(targets_by_dir, snippet_by_dir):
    """Each chapter dir gets a README listing its runnable targets and the
    documented snippets (signatures/pseudo-code folded in as reference)."""
    all_dirs = sorted(set(list(targets_by_dir.keys()) + list(snippet_by_dir.keys())))
    for d in all_dirs:
        lines = [f"# {d} — Qt 示例\n\n"]
        tgts = targets_by_dir.get(d, [])
        if tgts:
            lines.append("## 可运行示例\n\n")
            lines.append("| 目标 | 源文件 | 说明 |\n")
            lines.append("|---|---|---|\n")
            for tgt, src_rel, has_qrc, qrc in tgts:
                lines.append(f"| `{tgt}` | `{os.path.basename(src_rel)}` | "
                             f"构建运行见顶层 README；离屏验证 `QT_QPA_PLATFORM=offscreen tools/run_qt.sh {tgt}` |\n")
            lines.append("\n")
        snips = snippet_by_dir.get(d, [])
        if snips:
            lines.append("## 参考片段（签名/伪代码，未单独建目标）\n\n")
            for (idx, title, code) in snips:
                lines.append(f"### [{PAGES_META_PID if False else ''}block {idx}] {title}\n\n")
                lines.append("```cpp\n")
                lines.append(code.rstrip() + "\n")
                lines.append("```\n\n")
        write(os.path.join(REPO, d, "README.md"), "".join(lines))

def write_errata():
    lines = ["# ERRATA — 代码修正记录（Part3 Qt）\n\n",
             "本文件由 `tools/gen_part3.py` 自动生成，记录对 wiki 原始 Qt 代码块的每一处修正。\n",
             "格式：`[pageId:blockIdx]` 类型 → 原内容 → 修正后 → 依据。\n\n"]
    if not ERRATA:
        lines.append("（无修正）\n")
    else:
        for (pid, idx, kind, before, after, reason) in ERRATA:
            lines.append(f"## [{pid}:{idx}] {kind}\n\n")
            lines.append(f"- **原文**：`{before}`\n")
            lines.append(f"- **修正**：`{after}`\n")
            lines.append(f"- **依据**：{reason}\n\n")
    write(os.path.join(REPO, "docs", "ERRATA_part3.md"), "".join(lines))

if __name__ == "__main__":
    main()
