/ justapi / webtui / build.py
swadhinbiswas 🎨 docs: new webtui/ β€” from-scratch terminal docs site (WebTUI, 8-bit palette, dual sidebar, nerd icons, 5 themes) Β· 8/8/2026
74e99fc
199 lines Β· 7.7 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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python3
"""JustAPI docs build β€” WebTUI terminal static site generator.

Reads markdown from ../docs_site/src/content/docs, renders each page with
the WebTUI terminal template (navbar + dual sidebars + content), and writes
static HTML to dist/.
"""
import re
import shutil
import html as html_mod
from pathlib import Path

import markdown

ROOT = Path(__file__).parent
CONTENT = Path(__file__).parent.parent / "docs_site/src/content/docs"
DIST = ROOT / "dist"

# ─── Sidebar structure: (group, [(label, path), ...]) ───
SIDEBAR = [
    ("Getting Started", [
        ("Overview", "/getting-started/overview/"),
        ("Installation", "/getting-started/installation/"),
        ("First Steps", "/getting-started/first-steps/"),
        ("CLI Scaffolder", "/getting-started/cli-scaffolder/"),
        ("Migrate from FastAPI", "/getting-started/migrating-from-fastapi/"),
    ]),
    ("Tutorials", [
        ("Hello World", "/tutorials/hello-world/"),
        ("Path Parameters", "/tutorials/path-params/"),
        ("Query Parameters", "/tutorials/query-params/"),
        ("Request Body", "/tutorials/request-body/"),
        ("Error Handling", "/tutorials/error-handling/"),
        ("Dependencies", "/tutorials/dependency-injection/"),
        ("Middleware", "/tutorials/middleware/"),
        ("CORS", "/tutorials/cors/"),
        ("Databases", "/tutorials/database-integration/"),
        ("Background Tasks", "/tutorials/background-tasks/"),
        ("Testing", "/tutorials/testing/"),
        ("Static Files", "/tutorials/static-files/"),
    ]),
    ("Advanced", [
        ("Zero-GIL", "/advanced/zero-gil-architecture/"),
        ("Rust Core", "/advanced/rust-core-deep-dive/"),
        ("Native Fast Path", "/advanced/native-fast-path/"),
        ("Streaming Output", "/advanced/streaming-output/"),
        ("WebSockets", "/advanced/websockets-advanced/"),
        ("Templates", "/advanced/templates/"),
        ("Performance Tuning", "/advanced/performance-tuning/"),
        ("Resilience", "/advanced/resilience-patterns/"),
    ]),
    ("API Reference", [
        ("JustAPIApp", "/api-reference/justapiapp/"),
        ("Routing", "/api-reference/routing/"),
        ("Request", "/api-reference/request/"),
        ("Responses", "/api-reference/responses/"),
        ("Dependency Injection", "/api-reference/dependency-injection/"),
        ("Exceptions", "/api-reference/exceptions/"),
        ("Schema Validation", "/api-reference/schema-validation/"),
        ("WebSockets", "/api-reference/websockets/"),
        ("Background Tasks", "/api-reference/background-tasks/"),
        ("Scheduler", "/api-reference/scheduler/"),
        ("Database", "/api-reference/database/"),
        ("Testing Client", "/api-reference/testing-client/"),
        ("UploadFile", "/api-reference/uploadfile/"),
    ]),
    ("Deployment", [
        ("Docker", "/deployment/docker/"),
        ("Kubernetes", "/deployment/kubernetes-helm/"),
        ("Cloudflare Pages", "/deployment/cloudflare-pages/"),
        ("Production Checklist", "/deployment/production-checklist/"),
    ]),
    ("Security", [
        ("Security Policy", "/security/policy/"),
        ("OWASP", "/security/owasp-compliance/"),
        ("Secure Config", "/security/secure-configuration/"),
    ]),
    ("Observability", [
        ("Metrics", "/observability/metrics-monitoring/"),
        ("OpenTelemetry", "/observability/opentelemetry/"),
        ("Structured Logging", "/observability/structured-logging/"),
        ("Health Checks", "/observability/health-checks/"),
    ]),
    ("Reference", [
        ("CLI", "/reference/cli/"),
        ("Configuration", "/reference/configuration/"),
        ("API Stability", "/reference/api-stability/"),
        ("Release Notes", "/reference/release-notes/"),
        ("Glossary", "/reference/glossary/"),
    ]),
]

TEMPLATE = (ROOT / "templates/base.html").read_text()


def parse_frontmatter(text: str) -> tuple[dict, str]:
    """Extract YAML-ish frontmatter (title/description) and the body."""
    meta: dict = {}
    if text.startswith("---"):
        end = text.find("---", 3)
        if end != -1:
            raw = text[3:end]
            for line in raw.strip().splitlines():
                if ":" in line:
                    k, v = line.split(":", 1)
                    v = v.strip().strip('"').strip("'")
                    if v.startswith("[") or v.startswith("{"):
                        continue  # skip list/dict meta
                    meta[k.strip()] = v
            text = text[end + 3 :]
    return meta, text


def build_sidebar(active_path: str) -> str:
    parts = []
    for group, items in SIDEBAR:
        parts.append(f'<div class="ja-nav-group">{html_mod.escape(group)}</div>')
        for label, path in items:
            cls = ' class="active"' if path == active_path else ""
            parts.append(f'<a href="{path}"{cls}>{html_mod.escape(label)}</a>')
    return "\n".join(parts)


def build_toc(html: str) -> str:
    """Extract h2/h3 headings from rendered HTML for the right-side TOC."""
    links = []
    for m in re.finditer(r"<h([23])[^>]*>(.*?)</h\1>", html, re.S):
        level, title = m.group(1), re.sub(r"<[^>]+>", "", m.group(2))
        slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
        links.append(f'<a class="h{level}" href="#{slug}">{html_mod.escape(title)}</a>')
    return "\n".join(links)


def slugify_headers(html: str) -> str:
    """Add id attributes to h2/h3 for TOC anchors."""
    def repl(m):
        level, attrs, title = m.group(1), m.group(2), m.group(3)
        slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
        return f'<h{level} id="{slug}"{attrs}>{title}</h{level}>'
    return re.sub(r"<h([23])([^>]*)>(.*?)</h\1>", repl, html, flags=re.S)


def md_to_html(md_text: str) -> str:
    return markdown.markdown(
        md_text,
        extensions=["fenced_code", "tables", "codehilite", "toc", "attr_list"],
    )


def render_page(meta: dict, body: str, path: str) -> str:
    content_html = md_to_html(body)
    content_html = slugify_headers(content_html)
    title = meta.get("title", "JustAPI")
    description = meta.get("description", "JustAPI β€” Python web framework with a Rust core.")
    page = TEMPLATE
    page = page.replace("{{ title }}", html_mod.escape(title))
    page = page.replace("{{ description }}", html_mod.escape(description))
    page = page.replace("{{ content }}", content_html)
    page = page.replace("{{ sidebar }}", build_sidebar(path))
    page = page.replace("{{ toc }}", build_toc(content_html))
    return page


def build() -> None:
    if DIST.exists():
        shutil.rmtree(DIST)
    DIST.mkdir(parents=True)

    # Static assets
    (DIST / "css").mkdir(parents=True)
    shutil.copy(ROOT / "src/main.css", DIST / "css/main.css")
    shutil.copytree(ROOT / "css", DIST / "css/webtui")
    (DIST / "js").mkdir(parents=True)
    shutil.copy(ROOT / "src/main.js", DIST / "js/main.js")

    count = 0
    for md_file in sorted(CONTENT.rglob("*.md*")):
        rel = md_file.relative_to(CONTENT)
        if rel.name == "index.mdx":
            continue  # landing page handled separately
        meta, body = parse_frontmatter(md_file.read_text())
        url_path = "/" + str(rel.with_suffix("")).replace("\\", "/") + "/"
        out = DIST / str(rel).replace(".mdx", "").replace(".md", "") / "index.html"
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(render_page(meta, body, url_path))
        count += 1

    # Landing page (hand-built, no frontmatter)
    landing = (ROOT / "templates/landing.html").read_text()
    landing = landing.replace("{{ sidebar }}", build_sidebar("/"))
    landing = landing.replace("{{ toc }}", "")
    (DIST / "index.html").write_text(landing)

    print(f"built {count} docs pages + landing β†’ {DIST}")


if __name__ == "__main__":
    build()