From 8c494a59599727aa228b8961f960fcbd93703b70 Mon Sep 17 00:00:00 2001 From: watanabe-kohei-jp <283722319+watanabe-kohei-jp@users.noreply.github.com> Date: Sun, 17 May 2026 17:53:56 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E5=AE=8C=E5=85=A8=E7=89=88=EF=BC=88?= =?UTF-8?q?=E5=8D=98=E4=B8=80=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=EF=BC=89?= =?UTF-8?q?=E3=83=93=E3=83=AB=E3=83=89=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=97?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 各章の index.html から shared/ の CSS・JS とローカル画像を インライン化し、dist/<章名>.html として 1 ファイルに生成する build.py。 リポジトリ・ネット接続なしで開ける配布/オフライン閲覧用。 - build.py: Python 3 標準ライブラリのみ。NN-* 章を自動検出 - 正本は各章 index.html(shared/ 参照版)のまま。dist/ は git 管理外 - progress-strip.js は readyState 自前ガードのため defer 除去でも安全 - Web フォント(Google Fonts @import)は埋め込まない(既知の制約) - .gitignore に dist/ を追加。lecture.html→index.html の旧コメントも訂正 - README に「完全版をビルドする」手順を追記 No-Issue: 配布・オフライン閲覧用の単一ファイル生成 Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 5 +- README.md | 14 ++++++ build.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 build.py diff --git a/.gitignore b/.gitignore index c3e70e7..173920a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index dac0995..d750edd 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..cf7d02b --- /dev/null +++ b/build.py @@ -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. + 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. + # (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"" + + html = re.sub( + r'[^"]+)"[^>]*>\s*', + 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()) From 2d1556a86b014a8d69a248e540af9dee466a90a2 Mon Sep 17 00:00:00 2001 From: watanabe-kohei-jp <283722319+watanabe-kohei-jp@users.noreply.github.com> Date: Sun, 17 May 2026 17:57:56 +0900 Subject: [PATCH 2/3] =?UTF-8?q?build.py:=20script=20=E7=B5=82=E4=BA=86?= =?UTF-8?q?=E3=82=BF=E3=82=B0=E6=AD=A3=E8=A6=8F=E8=A1=A8=E7=8F=BE=E3=82=92?= =?UTF-8?q?=E5=A0=85=E7=89=A2=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL py/bad-tag-filter(high)の指摘に対応。`` リテラルが ``(空白入り)や大文字に非対応だったため、`` + re.IGNORECASE に変更。 No-Issue: PR #12 の CodeQL 指摘修正 Co-Authored-By: Claude Opus 4.7 (1M context) --- build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index cf7d02b..12d87fa 100644 --- a/build.py +++ b/build.py @@ -77,10 +77,12 @@ def js_repl(m: re.Match) -> str: js = resolve(src).read_text(encoding="utf-8-sig") return f"" + # 終了タグは ``(空白入り)や大文字も許容する html = re.sub( - r'[^"]+)"[^>]*>\s*', + r'[^"]+)"[^>]*>\s*', js_repl, html, + flags=re.IGNORECASE, ) # 3. のローカル画像 → data URI From b63c45509645a24d6ed89193f5d0483e9ae1063b Mon Sep 17 00:00:00 2001 From: watanabe-kohei-jp <283722319+watanabe-kohei-jp@users.noreply.github.com> Date: Sun, 17 May 2026 18:01:55 +0900 Subject: [PATCH 3/3] =?UTF-8?q?build.py:=20script=20=E3=82=BF=E3=82=B0?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=20CodeQL=20py/bad-tag-filter=20?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit script 要素全体()をマッチする正規表現は CodeQL に 脆弱なタグフィルタと判定される。開始タグ をそのまま閉じタグとして再利用する方式に変更。 あわせて、インライン化する JS 内の " --- build.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build.py b/build.py index 12d87fa..fe2d1f8 100644 --- a/build.py +++ b/build.py @@ -67,23 +67,23 @@ def css_repl(m: re.Match) -> str: html, ) - # 2. - # (defer 属性は落ちるが、progress-strip.js は readyState を自前で - # ガードしており、deck-stage.js は head 配置で問題ない) + # 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") - return f"" + # JS 内に "\n{js}\n" - # 終了タグは ``(空白入り)や大文字も許容する - html = re.sub( - r'[^"]+)"[^>]*>\s*', - js_repl, - html, - flags=re.IGNORECASE, - ) + html = re.sub(r'[^"]+)"[^>]*>', js_repl, html) # 3. のローカル画像 → data URI def img_repl(m: re.Match) -> str: