215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate nexus-docs.html from template.html + ALL docs/ files.
|
|
|
|
Usage: python build_html.py
|
|
Output: docs/nexus-docs.html (single self-contained HTML file)
|
|
|
|
Collects: .md .json .js .py .sh .txt .yaml .yml .toml .cfg .ini .conf .env .sql
|
|
Excludes: build_html.py, template.html, nexus-docs.html, binary files
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
|
|
DOCS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
TEMPLATE = os.path.join(DOCS_DIR, "template.html")
|
|
OUTPUT = os.path.join(DOCS_DIR, "nexus-docs.html")
|
|
|
|
# File extensions to include (text/documentation files)
|
|
INCLUDE_EXT = {
|
|
'.md', '.markdown',
|
|
'.json', '.jsonl',
|
|
'.js', '.ts', '.mjs',
|
|
'.py', '.sh', '.bash',
|
|
'.txt', '.csv',
|
|
'.yaml', '.yml',
|
|
'.toml', '.cfg', '.ini', '.conf',
|
|
'.env', '.example',
|
|
'.sql',
|
|
'.xml', '.html', '.css',
|
|
'.gitignore',
|
|
}
|
|
|
|
# Files to always exclude (by basename)
|
|
EXCLUDE_FILES = {
|
|
'build_html.py',
|
|
'template.html',
|
|
'nexus-docs.html',
|
|
}
|
|
|
|
# Directories to skip
|
|
SKIP_DIRS = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
|
|
|
|
# Group config: (directory_match, display_name, icon)
|
|
GROUP_CONFIG = [
|
|
("project", "Project", "📋"),
|
|
("design", "Design", "🎨"),
|
|
("changelog", "Changelog", "📝"),
|
|
("reports", "Reports", "📊"),
|
|
("team", "Team", "👥"),
|
|
("memory", "Memory", "🧠"),
|
|
("research", "Research", "🔬"),
|
|
("skills", "Skills", "⚡"),
|
|
]
|
|
|
|
# Fallback for unmatched dirs
|
|
FALLBACK_ICON = "📄"
|
|
|
|
|
|
def collect_all_files():
|
|
"""Walk docs/ and return list of (relative_path, full_path) sorted. Includes all text doc types."""
|
|
results = []
|
|
for root, dirs, files in os.walk(DOCS_DIR):
|
|
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
|
|
for f in sorted(files):
|
|
if f in EXCLUDE_FILES:
|
|
continue
|
|
ext = os.path.splitext(f)[1].lower()
|
|
if ext in INCLUDE_EXT:
|
|
full = os.path.join(root, f)
|
|
rel = os.path.relpath(full, DOCS_DIR).replace('\\', '/')
|
|
results.append((rel, full))
|
|
return results
|
|
|
|
|
|
def group_files(files):
|
|
"""Group files by top-level directory using GROUP_CONFIG."""
|
|
grouped = {}
|
|
|
|
for rel, full in files:
|
|
top = rel.split('/')[0] if '/' in rel else "Root"
|
|
|
|
# Match against config
|
|
matched = False
|
|
for dir_key, display_name, icon in GROUP_CONFIG:
|
|
if top == dir_key or top.startswith(dir_key):
|
|
key = display_name
|
|
if key not in grouped:
|
|
grouped[key] = {"icon": icon, "files": []}
|
|
grouped[key]["files"].append((rel, full))
|
|
matched = True
|
|
break
|
|
|
|
if not matched:
|
|
if top not in grouped:
|
|
# Find icon from config if partial match
|
|
icon = FALLBACK_ICON
|
|
for dir_key, _, ic in GROUP_CONFIG:
|
|
if dir_key in top:
|
|
icon = ic
|
|
break
|
|
grouped[top] = {"icon": icon, "files": []}
|
|
grouped[top]["files"].append((rel, full))
|
|
|
|
# Sort by GROUP_CONFIG order, then alphabetically for rest
|
|
ordered = []
|
|
for _, display_name, _ in GROUP_CONFIG:
|
|
if display_name in grouped:
|
|
ordered.append((display_name, grouped.pop(display_name)))
|
|
for k in sorted(grouped.keys()):
|
|
ordered.append((k, grouped[k]))
|
|
|
|
return ordered
|
|
|
|
|
|
def read_file(path):
|
|
try:
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
except Exception:
|
|
return "*Unable to read file*"
|
|
|
|
|
|
def build_data(groups):
|
|
"""Build NAV_GROUPS and CONTENT_MAP JS objects."""
|
|
nav_groups = []
|
|
content_map = {}
|
|
|
|
for name, info in groups:
|
|
files = []
|
|
for rel, full in info["files"]:
|
|
# Show full filename as label (with extension for non-md files)
|
|
label = rel.split('/')[-1]
|
|
# Remove .md extension only for markdown files
|
|
if label.endswith('.md'):
|
|
label = label[:-3]
|
|
files.append({"path": rel, "label": label})
|
|
# For non-md files, wrap content in a code block for proper rendering
|
|
raw = read_file(full)
|
|
ext = os.path.splitext(rel)[1].lower()
|
|
if ext == '.md':
|
|
content_map[rel] = raw
|
|
else:
|
|
lang = ext.lstrip('.')
|
|
content_map[rel] = f"# `{rel}`\n\n```{lang}\n{raw}\n```"
|
|
|
|
nav_groups.append({
|
|
"name": name,
|
|
"icon": info["icon"],
|
|
"collapsed": name == "Skills", # Skills collapsed by default
|
|
"files": files,
|
|
})
|
|
|
|
return nav_groups, content_map
|
|
|
|
|
|
def generate(nav_groups, content_map):
|
|
"""Read template, inject data, return final HTML."""
|
|
with open(TEMPLATE, 'r', encoding='utf-8') as f:
|
|
template = f.read()
|
|
|
|
nav_json = json.dumps(nav_groups, ensure_ascii=False, indent=None)
|
|
# content_map can be huge — compact serialization
|
|
content_json = json.dumps(content_map, ensure_ascii=False, indent=None)
|
|
|
|
# Inject data before the docApp() function
|
|
data_script = f"""<script>
|
|
// Auto-generated data — do not edit
|
|
const NAV_GROUPS = {nav_json};
|
|
const CONTENT_MAP = {content_json};
|
|
</script>
|
|
"""
|
|
|
|
# Insert data script right before the docApp function script
|
|
placeholder = "// __DATA_PLACEHOLDER__ will be replaced by build_html.py"
|
|
if placeholder in template:
|
|
html = template.replace(placeholder, f"// Data injected by build_html.py\nconst NAV_GROUPS = {nav_json};\nconst CONTENT_MAP = {content_json};")
|
|
else:
|
|
# Fallback: insert before closing </body>
|
|
html = template.replace("</body>", data_script + "</body>")
|
|
|
|
return html
|
|
|
|
|
|
def main():
|
|
import sys
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
|
|
print("Scanning docs/...")
|
|
files = collect_all_files()
|
|
md_count = sum(1 for f in files if f[0].endswith('.md'))
|
|
other_count = len(files) - md_count
|
|
print(f" Found {md_count} markdown + {other_count} other = {len(files)} total files")
|
|
|
|
print("Grouping...")
|
|
groups = group_files(files)
|
|
for name, info in groups:
|
|
print(f" {info['icon']} {name}: {len(info['files'])} files")
|
|
|
|
print("Reading content & building data...")
|
|
nav_groups, content_map = build_data(groups)
|
|
|
|
print("Generating HTML...")
|
|
html = generate(nav_groups, content_map)
|
|
|
|
with open(OUTPUT, 'w', encoding='utf-8') as f:
|
|
f.write(html)
|
|
|
|
size_mb = os.path.getsize(OUTPUT) / 1024 / 1024
|
|
print(f"\nDone: {OUTPUT}")
|
|
print(f"Size: {size_mb:.1f} MB")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|