-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstatic_generator.py
More file actions
178 lines (148 loc) · 5.99 KB
/
Copy pathstatic_generator.py
File metadata and controls
178 lines (148 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
"""aiseed web creator — 静的サイトジェネレーター
2つのコンテンツソースに対応:
1. site_data.json — GUI フォームで入力したデータ
2. content/*.md — Markdown ファイル(任意のエディタ/AI で作成可能)
Jinja2 テンプレート (_templates/) と組み合わせて HTML を出力する。
Claude API 不要で即座にビルドできる。
"""
import json, re
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
from markdown_it import MarkdownIt
from image_utils import load_json
def _parse_frontmatter(text: str) -> tuple[dict, str]:
"""YAML 風の front matter を簡易パースする(PyYAML 不要)。
---
title: Hello
template: post.html.j2
slug: hello
date: 2024-01-01
---
本文…
"""
meta: dict = {}
body = text
m = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
if m:
for line in m.group(1).splitlines():
if ":" in line:
k, v = line.split(":", 1)
meta[k.strip()] = v.strip()
body = text[m.end():]
return meta, body
def _load_content(content_dir: Path) -> list[dict]:
"""content/ 以下の .md を読み込み、メタ + HTML 本文を返す。"""
if not content_dir.exists():
return []
md = MarkdownIt("commonmark", {"html": True}).enable("table")
items = []
for md_path in sorted(content_dir.rglob("*.md")):
raw = md_path.read_text("utf-8")
meta, body = _parse_frontmatter(raw)
meta["_body_html"] = md.render(body)
# ファイルパスから既定値を補完
rel = md_path.relative_to(content_dir)
if "slug" not in meta:
meta["slug"] = md_path.stem
if "filename" not in meta:
meta["filename"] = str(rel.with_suffix(".html"))
# コレクション判定: content/blog/xxx.md → collection="blog"
if len(rel.parts) > 1:
meta["_collection"] = rel.parts[0]
items.append(meta)
return items
def render_site(project_dir: Path, log_fn=None) -> list[str]:
"""_templates/ 内の .j2 + site_data.json + content/*.md → HTML。
Returns: 生成したファイルの相対パス一覧。
"""
output_dir = project_dir / "output"
templates_dir = output_dir / "_templates"
content_dir = project_dir / "content"
data_file = project_dir / "site_data.json"
if not templates_dir.exists() or not list(templates_dir.rglob("*.j2")):
raise FileNotFoundError(
"テンプレートが見つかりません。先に「テンプレート生成」を実行してください。")
data = load_json(data_file, {})
# Markdown コンテンツを読み込み
content_items = _load_content(content_dir)
# コレクション別に分類して data に統合
collections: dict[str, list] = {}
page_contents: dict[str, dict] = {} # filename → content item
for item in content_items:
col = item.pop("_collection", None)
if col:
collections.setdefault(col, []).append(item)
else:
page_contents[item["filename"]] = item
# site_data のコレクションと Markdown コレクションをマージ
for col_name, col_items in collections.items():
existing = data.get(col_name, [])
existing_slugs = {i.get("slug") for i in existing}
for ci in col_items:
if ci.get("slug") not in existing_slugs:
existing.append(ci)
data[col_name] = existing
env = Environment(
loader=FileSystemLoader(str(templates_dir)),
autoescape=select_autoescape(["html"]),
keep_trailing_newline=True,
)
generated = []
for j2_path in sorted(templates_dir.rglob("*.j2")):
rel = j2_path.relative_to(templates_dir)
template_name = str(rel)
# base.j2 等のレイアウトテンプレートはスキップ
if rel.name == "base.j2":
continue
out_rel = rel.with_suffix("") # index.html.j2 → index.html
# _post.html.j2 等のコレクションテンプレート
if rel.name.startswith("_"):
generated.extend(
_render_collection(env, template_name, out_rel, data,
output_dir, log_fn))
continue
# ページテンプレート — Markdown の本文があればテンプレート変数に渡す
page_content = page_contents.get(str(out_rel), {})
tpl = env.get_template(template_name)
html = tpl.render(
site=data.get("site", {}),
data=data,
pages=data.get("pages", []),
content=page_content.get("_body_html", ""),
page=page_content,
)
dest = output_dir / out_rel
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(html, "utf-8")
generated.append(str(out_rel))
if log_fn:
log_fn(f" {out_rel}")
return generated
def _render_collection(env, template_name, out_rel, data, output_dir, log_fn):
"""ブログ記事・イベント等、1テンプレートから複数ページを生成する。"""
generated = []
tpl = env.get_template(template_name)
parent = out_rel.parent # blog, events 等
collection_key = str(parent)
items = data.get(collection_key, [])
for item in items:
if item.get("status") == "draft":
continue
slug = item.get("slug", "")
if not slug:
continue
dest_rel = parent / f"{slug}.html"
html = tpl.render(
site=data.get("site", {}),
data=data,
item=item,
content=item.get("_body_html", ""),
pages=data.get("pages", []),
)
dest = output_dir / dest_rel
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(html, "utf-8")
generated.append(str(dest_rel))
if log_fn:
log_fn(f" {dest_rel}")
return generated