diff --git a/.gitignore b/.gitignore index a691249..173920a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ node_modules/ __pycache__/ *.pyc .venv/ + +# --- 完全版ビルド成果物(build.py が生成。正本は各章の index.html)--- dist/ # --- Claude Code ローカル設定 --- @@ -25,7 +27,7 @@ dist/ # --- 旧 PPTx(HTML 路線へ移行済み、新規 PPTx は受け付けない) --- *.pptx -# --- 未使用の制作物アセット(実際に lecture.html から参照されているものだけ track する) --- +# --- 未使用の制作物アセット(実際に index.html から参照されているものだけ track する) --- # 過去の制作 / 試作画像は手元には残してリポでは track しない。 01-claude-code-intro/_assets/horse-harness.png 01-claude-code-intro/_assets/model-service-layers.png diff --git a/README.md b/README.md index 9883ff7..51008ad 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,20 @@ cd lectures --- +## 完全版(1 ファイル)をビルドする + +各章を 1 つの HTML にまとめた「完全版」を生成できます。`shared/` の CSS・JS と画像をすべて埋め込むので、**リポジトリもネット接続もなしで開けます**。メール添付・USB 配布・オフライン閲覧に。 + +```bash +python build.py +``` + +`dist/00-about.html` などが生成されます(`dist/` は git 管理外のビルド成果物)。正本は各章の `index.html`(`shared/` 参照版)のままなので、`shared/` や本文を変更したら `python build.py` で作り直します。依存は Python 3 標準ライブラリのみ。 + +> Web フォント(Google Fonts)だけは埋め込みません。完全オフラインではシステムフォントにフォールバックします(レイアウトは崩れません)。 + +--- + ## 一緒に作る — 貢献の入口 このリポジトリは **教える側と教わる側の境界をゆるくする** ことを意図しています。受講者も貢献者です。 diff --git a/build.py b/build.py new file mode 100644 index 0000000..fe2d1f8 --- /dev/null +++ b/build.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""build.py — 各章の index.html から「完全版」(単一ファイル)を生成する。 + +shared/ の CSS・JS と、ローカル画像(qr.svg / _assets/*.png)を HTML に +インライン化し、dist/<章名>.html として 1 ファイルにまとめる。 +リポジトリやネット接続なしで開ける、配布・オフライン閲覧用のファイル。 + +正本は各章の index.html(shared/ 参照版)のまま。dist/ はビルド成果物で +git 管理外。内容や shared/ を変更したら `python build.py` で再生成する。 + +既知の制約: + theme.css は Google Fonts を @import している。完全オフラインでは + Web フォントが読めず、システムフォントにフォールバックする + (レイアウトは崩れない)。フォント自体の埋め込みは行わない。 + +依存: Python 3 標準ライブラリのみ。 +使い方: リポジトリ直下で `python build.py` +""" +from __future__ import annotations + +import base64 +import mimetypes +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +DIST = ROOT / "dist" + +# 外部 / data URI は埋め込み対象外(そのまま残す) +_SKIP_PREFIXES = ("http://", "https://", "data:", "//") + + +def find_chapters() -> list[Path]: + """`NN-...` 形式で index.html を持つ章ディレクトリを返す。""" + return sorted(p.parent for p in ROOT.glob("[0-9][0-9]-*/index.html")) + + +def data_uri(path: Path) -> str: + mime, _ = mimetypes.guess_type(path.name) + if mime is None: + mime = "application/octet-stream" + b64 = base64.b64encode(path.read_bytes()).decode("ascii") + return f"data:{mime};base64,{b64}" + + +def inline(html: str, base_dir: Path) -> str: + """html 内のローカル CSS / JS / 画像参照をインライン化して返す。""" + + def resolve(ref: str) -> Path: + return (base_dir / ref).resolve() + + def is_local(ref: str) -> bool: + return not ref.startswith(_SKIP_PREFIXES) + + # 1. + def css_repl(m: re.Match) -> str: + href = m.group("href") + if not is_local(href): + return m.group(0) + css = resolve(href).read_text(encoding="utf-8-sig") + return f"" + + html = re.sub( + r'[^"]+)"\s*/?\s*>', + css_repl, + html, + ) + + # 2. はそのまま残り、インライン化したスクリプトを閉じる。 + # (終了タグを正規表現で扱わないことで、script 要素全体をマッチする + # 脆い正規表現パターンを避ける) + # defer 属性は落ちるが、progress-strip.js は readyState を自前で + # ガードしており、deck-stage.js は head 配置で問題ない。 + def js_repl(m: re.Match) -> str: + src = m.group("src") + if not is_local(src): + return m.group(0) + js = resolve(src).read_text(encoding="utf-8-sig") + # JS 内に "\n{js}\n" + + html = re.sub(r'[^"]+)"[^>]*>', js_repl, html) + + # 3. のローカル画像 → data URI + def img_repl(m: re.Match) -> str: + src = m.group("src") + if not is_local(src): + return m.group(0) + return m.group(0).replace(f'src="{src}"', f'src="{data_uri(resolve(src))}"') + + html = re.sub(r']*\bsrc="(?P[^"]+)"[^>]*>', img_repl, html) + + return html + + +def main() -> int: + # Windows の既定コンソール(cp932)でも日本語を出せるようにする + try: + sys.stdout.reconfigure(encoding="utf-8") + except (AttributeError, ValueError): + pass + + chapters = find_chapters() + if not chapters: + print("章ディレクトリ(NN-.../index.html)が見つかりません。", file=sys.stderr) + return 1 + + DIST.mkdir(exist_ok=True) + ok = True + print(f"完全版をビルド中({len(chapters)} 章)...\n") + + for ch in chapters: + html = (ch / "index.html").read_text(encoding="utf-8-sig") + built = inline(html, ch) + + # 未解決のローカル参照(../ や _assets/)が残っていないか検証 + leftover = sorted(set(re.findall( + r'(?:href|src)="((?:\.\./|\./|_assets/)[^"]*)"', built))) + + out = DIST / f"{ch.name}.html" + out.write_text(built, encoding="utf-8") + size_kb = out.stat().st_size / 1024 + + if leftover: + ok = False + print(f" [NG] {out.relative_to(ROOT)} ({size_kb:,.0f} KB)") + print(f" 未解決のローカル参照: {leftover}") + else: + print(f" [OK] {out.relative_to(ROOT)} ({size_kb:,.0f} KB)") + + print() + if not ok: + print("一部の参照を埋め込めませんでした。上記を確認してください。", file=sys.stderr) + return 1 + print(f"完了: dist/ に {len(chapters)} ファイルを生成しました。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())