#!/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"""
"""
# 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