Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ __pycache__/
*.pyc
.venv/

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

# --- Claude Code ローカル設定 ---
.claude/settings.local.json

# --- 旧 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
142 changes: 142 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/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>...</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")
return f"<script>\n{js}\n</script>"

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

Check failure

Code scanning / CodeQL

Bad HTML filtering regexp High

This regular expression does not match script end tags like </script >.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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