teaching-handbook
<SKILL_DIR>代表本 SKILL.md 所在資料夾。
把 .docx / .md / .pptx 教學講義轉成側邊欄風格的教學網頁。
範圍邊界(重要)
本 skill 只負責機械轉檔(Stage 1):把原檔的文字、圖、結構忠實搬到 HTML,不改寫、不美化、不重組。
內容理解 / 改寫 / 美化(Stage 2)不在本 skill 範圍。原因:每份簡報的領域、讀者、風格差異太大,強行寫死自動規則只會把大部分簡報搞砸。Stage 2 由使用者在跑完轉檔後另起對話、依當份簡報的具體需求請 Claude 處理。
未來的維護者:不要把美化規則寫進這個 skill——本 skill 定位是忠實機械轉檔,美化屬於下游另一層。
若使用者指名 codelab / Codelabs 風格,本 skill 不處理,改用 codelab-handout(強意見視覺設計路線)。
硬規則(不可違反)
- 必須使用原始檔(
.docx/.md/.pptx),不接受先轉過的.htm/.html(否則圖片會糊)。 - 不要改寫
scripts/下的 Python 邏輯,直接呼叫即可。 - 輸出檔名以輸入檔名為基底(例如
我的講義.docx→我的講義.html),不要預設index.html,避免覆蓋專案主頁。 - 若目標輸出檔已存在,先向使用者確認再覆蓋。
依套件
依賴由各腳本的 inline metadata(PEP 723)宣告,uv run 會自動安裝,無需手動裝套件。
執行 SOP
步驟 1:確認輸入檔
- 使用者提供
.docx→ 走docx_converter.py - 使用者提供
.md→ 走md_converter.py - 使用者提供
.pptx→ 走pptx_converter.py - 其他副檔名 → 停下來問使用者
步驟 2:確認參數(都有預設值,可略)
--title:瀏覽器分頁標題(預設教學手冊,md 預設Document)--sidebar-title:側邊欄標題(docx 預設教學手冊導航;pptx 預設投影片目錄)--no-notes(僅 pptx):不納入講者備註。預設納入。- 輸出檔名:省略則自動用輸入檔名 +
.html
使用者若沒主動提,直接用預設值,不要反覆追問。
步驟 3:執行轉換
DOCX:
uv run <SKILL_DIR>/scripts/docx_converter.py "<input.docx>" --title "<標題>" --sidebar-title "<側邊欄標題>"
Markdown:
uv run <SKILL_DIR>/scripts/md_converter.py "<input.md>" --title "<標題>"
PPTX:
uv run <SKILL_DIR>/scripts/pptx_converter.py "<input.pptx>" --title "<標題>" --sidebar-title "<側邊欄標題>"
步驟 4:回報結果
告訴使用者輸出檔路徑,讓他可以打開檢查。不用自行開啟瀏覽器。
產出的網頁功能
- 左側 280px 固定側邊欄,自動從
h1/h2/h3生成目錄(Scroll Spy 高亮目前章節) <<收折按鈕:側邊欄縮成 60px,主區域擴展到 1400pxA- / 100% / A+字體縮放(預設 1.15x)- 程式碼區塊(單格表格或
<pre>)自動加「複製」按鈕 - 圖片響應式(
max-width: 100%),保留 Word 原始高畫質
檔案結構
<SKILL_DIR>/
├── SKILL.md ← 本文件
└── scripts/
├── docx_converter.py ← .docx 入口
├── md_converter.py ← .md 入口
├── pptx_converter.py ← .pptx 入口
├── style_injector.py ← 核心樣式 + 導航 + 複製按鈕引擎(上游、不動)
└── _polish.py ← 共用 helper:sidebar 搜尋框 + 圖片 lazy load
style_injector.py 自成一體,三個 converter 皆透過 import style_injector 呼叫它。
_polish.py 是 converter-side 的共用增強,與 style_injector.py 無關(後者保持原樣)。
三個 converter 共享的功能(透過 _polish.py)
下列功能無論 docx / md / pptx 都會生效:
- 圖片
loading="lazy":所有<img>自動帶 lazy 屬性,大檔載入快 - Sidebar 重建:清空
style_injector.py生成的 nav,用文件中所有 H1/H2/H3 重建(按文件順序)。修復「1. 章節」「純中文標題」等不符合上游 regex 的 heading 被 sidebar 漏掉的問題 - Sidebar 即時搜尋:在重建後的
#sidebar-nav上方注入<input id="th-search">,輸入即時過濾目錄項
執行時機:converter 在 body 末端塞 <script>,於 window.load 後執行(此時 style_injector.py 已跑完,nav 已被它的 regex 邏輯填好)。
為什麼放在 _polish.py 而非 style_injector.py:硬規則 #2 不動上游 style_injector.py。converter-side 增強統一抽到 _polish.py。
為什麼是「清空重建」而非「補登」
上游 style_injector.py sidebar JS 用 regex ^(\d+(\.\d+)?)(\s|$) || ^[A-Z](\s|$) 過濾 H2/H3。對下列常見情境全部會誤殺:
- markdown 標題「
1. 章節」(digit + dot + space,regex 在 dot 後 require digit,失敗) - 純中文 H2 / H3(既無數字也無大寫字母前綴)
- docx heading 用中文起頭
如果只「補登被漏的」,當 H1(永遠通過)和 H2(中文,被漏)交錯時,順序會錯(既有 H1 在前,後補的 H2 全部塞最後)。所以必須清空 nav 整個重建,按 querySelectorAll('h1, h2, h3') 的 document order 全列。
ID 兼容性:style_injector.py 給通過 regex 的 heading assign id="section-N",scroll-spy IntersectionObserver 用這些 ID 對應 nav-link。重建邏輯重用既有 id(若 heading 已有 id 則保留),新加的 heading assign th-section-N。Observer 端比對 href === '#' + id,新舊 ID 都對得上 → scroll-spy 正常。
順序保證:document order 由 querySelectorAll 保證,重建後 sidebar 順序 = 文件順序,與單純 append 在 H1/H2/H3 交錯文件的順序錯亂問題隔絕。
驗證(2026-04-25):
LSpe2.md(1 H1 + 15 H2 + 7 H3 共 23 個 heading):之前 sidebar 1 條,現在 23 條 ✅防災宣導_SROI.docx(10 H1):10 條 → 10 條,無 regression ✅金融大數據_v0.pptx(10 H2 「01 - 標題」):10 條 → 10 條,無 regression ✅
不要這樣修:改 — 違反硬規則 #2。style_injector.py 的 regex 一勞永逸_polish.py 的清空重建是正確的繞道方式。
pptx 轉換原則
- 每張投影片 = 一個
<h2>章節(側邊欄自動列出) - 章節分隔投影片 →
<h1>(無內容、無 bullets、≤1 圖、或 layout 名稱含section header/title slide/chapter) - 投影片標題空白 → 用
Slide N標示 - 投影片內文 →
<ul>(短)或<p>(長) - 投影片內嵌圖片 → 以 base64 data URI 內嵌、
loading="lazy"、80×80 縮圖橫排 - 講者備註 → 預設納入(教學型 pptx 常把真正內容放在備註);用
--no-notes可關閉 - Sidebar 自動加搜尋框(輸入即時過濾目錄項)
- 不做投影片轉圖、不還原版面 —— 這是重排器,不是螢幕截圖器
- 動畫、轉場、嵌入影片會丟失(pptx → HTML 的必然代價)
pptx 採坑紀錄(2026-04-25 實測 12 頁簡報)
下面是「為什麼 pptx_converter.py 看起來怪怪的」的解答。未來除錯前先讀完,避免把對的東西改壞。
1. 標題會被強制加 01 - 前綴 — 這是故意的
現象:pptx_converter.py 的 _render_slide_html 會把 投影片標題 改成 01 - 投影片標題 才送進 HTML。
原因:上游 style_injector.py 第 430 行 sidebar nav 生成 JS 用 regex 過濾 h2/h3 標題:
const match = text.match(/^(\d+(\.\d+)?)(\s|$)/) || text.match(/^[A-Z](\s|$)/);
if (match || header.tagName === 'H1') { /* add to sidebar */ }
只接受「數字 + 空白開頭」(如 1 章 1.1 節)或「單一大寫字母 + 空白開頭」(如 A 概論)。純中文標題全部被略過 → sidebar 變空白。
對策:converter 端強制前綴 f"{i:02d} - {title}"(不動上游腳本,符合硬規則 #2)。
不要這樣修:改 — 違反硬規則 #2,且會影響 docx/md 流程。style_injector.py 把 regex 放寬
2. 圖片預設縮成 80×80 縮圖橫排 — 這也是故意的
現象:投影片裡的圖片不是大圖內嵌,是 <div class="pptx-thumbs"> 裡的 80×80 小方塊橫排。
原因:PowerPoint 教學簡報通常每張投影片埋 5–10 張 256×256 的裝飾圖示(流程箭頭、燈泡、章節標誌等)—— 不是「正文圖」。如果當大圖內嵌,每張佔約 300px 垂直空間,累積就一片空白海。實測 12 頁簡報共 32 張圖、所有都是 256×256,幾乎全是裝飾。
對策:用 flex + 80×80 縮圖排在標題下方,視覺上是一行 icons。
不要這樣修:改回每張一行的大圖呈現 — 會讓所有教學型 pptx 變成空白海。如果未來真遇到「圖才是主角」的 pptx(例如設計作品集),應該另開新 skill(如 pptx-gallery),別改這個。
3. 過濾純數字/單字母的短 bullet
現象:_collect_body_paragraphs 會跳過長度 ≤ 2 且為純數字或純字母的文字。
原因:PowerPoint 投影片底部常有頁碼("2"、"3")或裝飾編號,會被 python-pptx 當成一般文字框讀進來,污染 bullet 清單。
不要這樣修:移掉這個過濾 — 除非使用者明確表示需要保留頁碼。
4. 上游 style_injector 的兩處來源專案特定 hack — 保留原樣
上游 style_injector.py 含兩處來源專案特定 hack(emoji 狀態色替換、「執行步驟」表格的 copy-btn 排除),對一般文件無害、幾乎不觸發;依硬規則 #2 保留原樣,不要清理。
教訓(給未來的我)
「LLM 按按鈕」型 skill 並非「加個檔就完事」。下游 style_injector.py 有隱性假設(標題要數字開頭、image 要適合大圖呈現),新加的 converter 必須學會這些假設才能對接。設計上的拗口都是有原因的,不要看到就想「優化」掉。
pptx 大檔擴張(2026-04-25 加,因應 200+ 頁簡報)
為了讓大檔(50+ 頁)也好用,pptx_converter.py 內建章節偵測:
章節偵測 → <h1>
_is_chapter_slide() 判斷規則:
- 投影片 layout 名稱含
section header/section divider/title slide/chapter→ 是章節 - 或 投影片無 bullets、無 long paragraphs、且 ≤1 張圖 → 是章節
章節投影片用 <h1>(不加 01 - 前綴,因 H1 不需 regex 匹配)。style_injector.py 自動把 H1 列為 sidebar 頂層(level-1,含上方分隔線)—— 這是上游已經支援的功能,我們只是讓 pptx 能觸發它。
不要這樣修:~~改用 H3 區分章節~~ — H3 受 regex 限制,且 sidebar 顯示太小看不出階層。
lazy load 與 sidebar 搜尋已移至三 converter 共享的 _polish.py,見上節「三個 converter 共享的功能」。