SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
5.8 KB · 145 lines swift
Raw Blame History
1//2//  DocsCommand.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import ArgumentParser11import Foundation12import ZQEngine1314/// `metrika-cli docs --output docs/` — renders the shared command15/// reference (the same registry behind the app's Manual pane and the16/// console `help`) into a self-contained static site.17struct DocsCommand: ParsableCommand {18    static let configuration = CommandConfiguration(19        commandName: "docs",20        abstract: "Generate the static documentation site from the command reference."21    )2223    @Option(name: .shortAndLong, help: "Output directory.")24    var output: String = "docs"2526    func run() throws {27        let directory = URL(fileURLWithPath: output, isDirectory: true)28        try FileManager.default.createDirectory(29            at: directory, withIntermediateDirectories: true30        )31        let html = Self.render()32        try Data(html.utf8).write(to: directory.appendingPathComponent("index.html"))33        print("wrote \(output)/index.html (\(ZQCommandReference.all.count) commands)")34    }3536    static func escape(_ text: String) -> String {37        text.replacingOccurrences(of: "&", with: "&amp;")38            .replacingOccurrences(of: "<", with: "&lt;")39            .replacingOccurrences(of: ">", with: "&gt;")40    }4142    static func render() -> String {43        var nav = ""44        var body = ""4546        for category in ZQCommandReference.categories {47            let docs = ZQCommandReference.all.filter { $0.category == category }48            guard !docs.isEmpty else { continue }49            nav += "<h3>\(escape(category))</h3><ul>"50            body += "<h2 class=\"category\">\(escape(category))</h2>\n"51            for doc in docs {52                nav += "<li><a href=\"#\(doc.verb)\"><code>\(doc.verb)</code></a></li>"53                body += renderCommand(doc)54            }55            nav += "</ul>"56        }5758        return """59        <!DOCTYPE html>60        <html lang="en">61        <head>62        <meta charset="utf-8">63        <meta name="viewport" content="width=device-width, initial-scale=1">64        <title>Metrika — Command Reference</title>65        <style>66        :root { color-scheme: light dark;67          --accent: #4f8cff; --muted: #7a8199; --border: #d9dce6;68          --code-bg: rgba(120, 130, 180, 0.12); }69        @media (prefers-color-scheme: dark) {70          :root { --border: #2c3145; } }71        * { box-sizing: border-box; }72        body { margin: 0; font: 15px/1.55 -apple-system, "Helvetica Neue", sans-serif;73          display: flex; min-height: 100vh; }74        nav { width: 230px; flex-shrink: 0; padding: 24px 18px; overflow-y: auto;75          position: sticky; top: 0; height: 100vh; border-right: 1px solid var(--border); }76        nav h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;77          color: var(--muted); margin: 18px 0 6px; }78        nav ul { list-style: none; margin: 0; padding: 0; }79        nav a { text-decoration: none; color: inherit; }80        nav a:hover code { color: var(--accent); }81        main { flex: 1; max-width: 760px; padding: 32px 40px 80px; }82        h1 { font-size: 28px; margin: 0 0 4px; }83        .subtitle { color: var(--muted); margin-bottom: 28px; }84        h2.category { font-size: 13px; text-transform: uppercase; letter-spacing: .08em;85          color: var(--muted); border-bottom: 1px solid var(--border);86          padding-bottom: 6px; margin-top: 44px; }87        article { margin: 26px 0; }88        article h2 { font-size: 20px; margin: 0 0 2px; font-family: ui-monospace, monospace; }89        .abbr { font-size: 12px; color: var(--muted); margin-left: 8px; }90        pre { background: var(--code-bg); border-radius: 8px; padding: 10px 14px;91          overflow-x: auto; font: 13px ui-monospace, "SF Mono", monospace; }92        code { font: 13px ui-monospace, "SF Mono", monospace; }93        table { border-collapse: collapse; margin: 8px 0; }94        td { padding: 3px 14px 3px 0; vertical-align: top; font-size: 14px; }95        td:first-child { font-family: ui-monospace, monospace; white-space: nowrap; }96        .notes { color: var(--muted); font-size: 14px; }97        footer { color: var(--muted); font-size: 12px; margin-top: 56px;98          border-top: 1px solid var(--border); padding-top: 12px; }99        </style>100        </head>101        <body>102        <nav>\(nav)</nav>103        <main>104        <h1>Metrika</h1>105        <p class="subtitle">Command reference — Stata-class syntax, GPU-accelerated by Apple Silicon.106        Every command follows one grammar:107        <code>command [varlist] [if] [in] [, options]</code></p>108        \(body)109        <footer>© 2026 Simon-Pierre Boucher. Generated from the in-app command110        reference by <code>metrika-cli docs</code>.</footer>111        </main>112        </body>113        </html>114        """115    }116117    static func renderCommand(_ doc: ZQCommandDoc) -> String {118        var html = "<article id=\"\(doc.verb)\">"119        html += "<h2>\(escape(doc.verb))"120        if let abbreviation = doc.abbreviation {121            html += "<span class=\"abbr\">abbreviation: \(escape(abbreviation))</span>"122        }123        html += "</h2>"124        html += "<p>\(escape(doc.summary))</p>"125        html += "<pre>\(escape(doc.syntax))</pre>"126        if !doc.options.isEmpty {127            html += "<table>"128            for option in doc.options {129                html += "<tr><td>\(escape(option.name))</td><td>\(escape(option.meaning))</td></tr>"130            }131            html += "</table>"132        }133        if !doc.examples.isEmpty {134            html += "<pre>"135                + doc.examples.map { ". " + escape($0) }.joined(separator: "\n")136                + "</pre>"137        }138        if let notes = doc.notes {139            html += "<p class=\"notes\">\(escape(notes))</p>"140        }141        html += "</article>"142        return html143    }144}145