252 lines
9.9 KiB
Python
252 lines
9.9 KiB
Python
#!/opt/homebrew/bin/python3.11
|
||
"""Build HTML documentation from module source files."""
|
||
import os, markdown, re, glob, shutil
|
||
|
||
MODULES_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "API_WORKSPACE", "modules")
|
||
DOC_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "doc")
|
||
DOC_DEV_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "doc_developer")
|
||
|
||
# User-facing modules (no developer content)
|
||
USER_MODULES = {
|
||
"01_auth", "02_health", "03_register", "04_lookup", "05_process",
|
||
"06_search", "07_identity", "08_identity_agent", "08_media",
|
||
"09_tmdb", "10_pipeline", "12_agent", "13_config",
|
||
}
|
||
|
||
|
||
def md_to_html(md_text: str) -> str:
|
||
"""Convert Markdown to HTML."""
|
||
html = markdown.markdown(md_text, extensions=['fenced_code', 'tables', 'codehilite'])
|
||
# Wrap tables
|
||
html = re.sub(r'<table>', '<table class="table">', html)
|
||
return html
|
||
|
||
def build_index(files, dev=False):
|
||
"""Build index.html."""
|
||
links = []
|
||
for fname in sorted(files):
|
||
name = os.path.splitext(fname)[0]
|
||
label = MODULE_LABELS.get(name, name.replace("_", " ").title())
|
||
if "|" in label:
|
||
cn, en = label.split("|", 1)
|
||
else:
|
||
cn, en = label, ""
|
||
html_name = fname.replace(".md", ".html")
|
||
links.append(f'<tr onclick="window.location=\'{html_name}\'" style="cursor:pointer"><td class="cn">{cn}</td><td class="en">{en}</td></tr>')
|
||
|
||
title = "Momentry API 開發者文件" if dev else "Momentry API 文件"
|
||
subtitle = "開發者專用" if dev else "API 參考手冊 — 登入後可瀏覽各模組文件"
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="zh-TW">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>{title}</title>
|
||
<style>
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }}
|
||
.container {{ max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }}
|
||
h1 {{ font-size: 28px; margin-bottom: 8px; }}
|
||
p.subtitle {{ color: #666; margin-bottom: 24px; }}
|
||
table {{ width: 100%; border-collapse: collapse; }}
|
||
tr {{ border-bottom: 1px solid #eee; }}
|
||
tr:last-child {{ border: none; }}
|
||
td {{ padding: 10px 0; }}
|
||
td.cn {{ width: 140px; font-weight: 600; color: #333; }}
|
||
td.en {{ color: #666; font-size: 14px; }}
|
||
a {{ color: #0066cc; text-decoration: none; display: block; }}
|
||
a:hover td {{ background: #f8f8f8; border-radius: 4px; }}
|
||
.topbar {{ display: flex; justify-content: space-between; align-items: baseline; }}
|
||
.logout-btn {{ font-size: 13px; color: #999; text-decoration: none; }}
|
||
.logout-btn:hover {{ color: #cc0000; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="topbar">
|
||
<h1>{title}</h1>
|
||
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{{method:'POST'}}).then(()=>window.location.reload());return false">Logout</a>
|
||
</div>
|
||
<p class="subtitle">{subtitle}</p>
|
||
<table>{"".join(links)}</table>
|
||
</div>
|
||
</body>
|
||
</html>"""
|
||
|
||
MODULE_LABELS = {
|
||
"01_auth": "安全認證|Authentication",
|
||
"02_health": "健康檢查|Health",
|
||
"03_register": "檔案註冊|File Registration",
|
||
"04_lookup": "檔案屬性查詢|File Lookup",
|
||
"05_process": "處理流程|Processing",
|
||
"06_search": "搜尋功能|Search",
|
||
"07_identity": "身份識別|Identity",
|
||
"08_identity_agent": "智能身份綁定|Smart Identity Binding",
|
||
"08_media": "串流與截圖|Streaming & Thumbnails",
|
||
"09_tmdb": "TMDb 整合|TMDb Integration",
|
||
"10_pipeline": "生產線|Pipeline",
|
||
"11_error_codes": "錯誤碼|Error Codes",
|
||
"12_agent": "智慧代理|AI Agents",
|
||
"13_config": "系統設定|System Config",
|
||
}
|
||
|
||
def build_html(md_text: str, title: str) -> str:
|
||
"""Wrap MD content in HTML page."""
|
||
content = md_to_html(md_text)
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>{title} - Momentry API Docs</title>
|
||
<style>
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }}
|
||
.container {{ max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }}
|
||
h1 {{ font-size: 24px; margin: 24px 0 12px; }}
|
||
h2 {{ font-size: 20px; margin: 20px 0 10px; color: #222; }}
|
||
h3 {{ font-size: 16px; margin: 16px 0 8px; color: #444; }}
|
||
p {{ line-height: 1.6; margin: 8px 0; }}
|
||
table {{ border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }}
|
||
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
|
||
th {{ background: #f0f0f0; font-weight: 600; }}
|
||
code {{ background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }}
|
||
pre {{ background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }}
|
||
pre code {{ background: none; padding: 0; }}
|
||
a {{ color: #0066cc; }}
|
||
.back {{ display: inline-block; margin-bottom: 20px; color: #666; }}
|
||
.back:hover {{ color: #333; }}
|
||
.topbar {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }}
|
||
.logout-btn {{ font-size: 13px; color: #999; text-decoration: none; }}
|
||
.logout-btn:hover {{ color: #cc0000; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="topbar">
|
||
<a class="back" href="index.html">← Back to index</a>
|
||
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{{method:'POST'}}).then(()=>window.location.reload());return false">Logout</a>
|
||
</div>
|
||
{content}
|
||
</div>
|
||
</body>
|
||
</html>"""
|
||
|
||
def login_page() -> str:
|
||
return """<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Login - Momentry Docs</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||
button:hover { background: #0052a3; }
|
||
.btn-logout { background: #888; margin-top: 8px; font-size: 13px; padding: 6px; }
|
||
.btn-logout:hover { background: #666; }
|
||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||
.success { color: #006600; font-size: 13px; margin-bottom: 12px; display: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<h1>Momentry Docs</h1>
|
||
<form id="loginForm">
|
||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||
<input type="password" id="password" placeholder="Password" value="" required>
|
||
<div class="error" id="error">Invalid credentials</div>
|
||
<button type="submit">Login</button>
|
||
<button type="button" class="btn-logout" onclick="logout()">Logout (clear session)</button>
|
||
<div class="success" id="logoutMsg">Session cleared</div>
|
||
</form>
|
||
</div>
|
||
<script>
|
||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||
e.preventDefault();
|
||
const resp = await fetch('/api/v1/auth/login', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
username: document.getElementById('username').value,
|
||
password: document.getElementById('password').value
|
||
})
|
||
});
|
||
if (resp.ok) {
|
||
window.location.href = '/doc/index.html';
|
||
} else {
|
||
document.getElementById('error').style.display = 'block';
|
||
}
|
||
};
|
||
async function logout() {
|
||
const resp = await fetch('/api/v1/auth/logout', { method: 'POST' });
|
||
if (resp.ok) {
|
||
document.getElementById('logoutMsg').style.display = 'block';
|
||
document.getElementById('error').style.display = 'none';
|
||
setTimeout(() => window.location.reload(), 1000);
|
||
}
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>"""
|
||
|
||
def main():
|
||
# Clean and recreate doc dirs
|
||
for d in [DOC_DIR, DOC_DEV_DIR]:
|
||
if os.path.exists(d):
|
||
shutil.rmtree(d)
|
||
os.makedirs(d)
|
||
|
||
md_files = sorted(glob.glob(os.path.join(MODULES_DIR, "*.md")))
|
||
if not md_files:
|
||
print(f"No MD files found in {MODULES_DIR}")
|
||
return
|
||
|
||
user_html = []
|
||
dev_html = []
|
||
for md_path in md_files:
|
||
with open(md_path) as f:
|
||
md_text = f.read()
|
||
fname = os.path.basename(md_path)
|
||
stem = os.path.splitext(fname)[0]
|
||
|
||
# Skip template
|
||
if stem == "_template":
|
||
continue
|
||
|
||
# Skip error codes (developer-only)
|
||
if stem == "11_error_codes":
|
||
dev_only = True
|
||
else:
|
||
dev_only = stem not in USER_MODULES
|
||
|
||
title = stem.replace("_", " ").title()
|
||
html = build_html(md_text, title)
|
||
|
||
if dev_only:
|
||
out_path = os.path.join(DOC_DEV_DIR, fname.replace(".md", ".html"))
|
||
with open(out_path, "w") as f:
|
||
f.write(html)
|
||
dev_html.append(fname)
|
||
print(f" [dev] {fname}")
|
||
else:
|
||
out_path = os.path.join(DOC_DIR, fname.replace(".md", ".html"))
|
||
with open(out_path, "w") as f:
|
||
f.write(html)
|
||
user_html.append(fname)
|
||
print(f" [doc] {fname}")
|
||
|
||
# Build indexes + login page
|
||
for d, files, label in [(DOC_DIR, user_html, "User"), (DOC_DEV_DIR, dev_html, "Dev")]:
|
||
index = build_index(files)
|
||
with open(os.path.join(d, "index.html"), "w") as f:
|
||
f.write(index)
|
||
with open(os.path.join(d, "login.html"), "w") as f:
|
||
f.write(login_page())
|
||
print(f" {label}: {len(files)} pages -> {d}")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|