Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ node_modules/
__pycache__/
*.pyc
.venv/

# --- 完全版ビルド成果物(build.py が生成。正本は各章の index.html)---
dist/

# --- Claude Code ローカル設定 ---
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)だけは埋め込みません。完全オフラインではシステムフォントにフォールバックします(レイアウトは崩れません)。

---

## 一緒に作る — 貢献の入口

このリポジトリは **教える側と教わる側の境界をゆるくする** ことを意図しています。受講者も貢献者です。
Expand Down
144 changes: 144 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
@@ -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. <link rel="stylesheet" href="..."> → <style>...</style>
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"<style>\n{css}\n</style>"

html = re.sub(
r'<link\s+rel="stylesheet"\s+href="(?P<href>[^"]+)"\s*/?\s*>',
css_repl,
html,
)

# 2. <script src="..."> の開始タグだけを差し替え、ソースを流し込む。
# 元の </script> はそのまま残り、インライン化したスクリプトを閉じる。
# (終了タグを正規表現で扱わないことで、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 内に "</script" があると HTML パーサが script を誤って閉じる。
# "<\/script" は JS 上は等価(文字列・正規表現・コメントいずれでも安全)。
js = js.replace("</script", "<\\/script")
return f"<script>\n{js}\n"

html = re.sub(r'<script\s+src="(?P<src>[^"]+)"[^>]*>', js_repl, html)

# 3. <img ... src="..."> のローカル画像 → 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'<img\b[^>]*\bsrc="(?P<src>[^"]+)"[^>]*>', 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())
Loading