use std::fmt::Write as _;
use tclrs::names::{self, Entry};
fn main() {
let commands = names::commands();
let ensembles: Vec<Ensemble> = ["string", "array", "dict", "info"]
.iter()
.map(|name| Ensemble::probe(name))
.collect();
let conversions = conversions();
let operator_count: usize = tclrs::expr::LEVELS.iter().map(|l| l.len()).sum();
let sub_total: usize = ensembles.iter().map(|e| e.subs.len()).sum();
let sub_ok: usize = ensembles.iter().map(Ensemble::implemented).sum();
let version = env!("CARGO_PKG_VERSION");
let count = commands.len();
let command_rows = rows(names::CORPUS.iter().map(|e: &Entry| {
format!(
"<tr><td><code>{}</code></td><td><code>{}</code></td><td>{}</td></tr>",
escape(e.name),
escape(e.synopsis),
escape(e.summary)
)
}));
let ensemble_tables = ensembles.iter().map(Ensemble::table).collect::<String>();
let operator_rows = rows(tclrs::expr::LEVELS.iter().enumerate().map(|(i, level)| {
let ops = level
.iter()
.map(|(text, _)| format!("<code>{}</code>", escape(text)))
.collect::<Vec<_>>()
.join(" ");
let assoc = if level.iter().any(|(text, _)| *text == "**") {
"right"
} else {
"left"
};
format!(
"<tr><td>{}</td><td>{ops}</td><td>{assoc}</td></tr>",
tclrs::expr::LEVELS.len() - i
)
}));
let class_cells = tclrs::cmd_string::CLASSES
.iter()
.map(|c| format!("<code>{}</code>", escape(c)))
.collect::<Vec<_>>()
.join(" · ");
let conversion_rows = rows(conversions.iter().map(|(conv, state)| {
format!(
"<tr><td><code>%{}</code></td><td>{}</td></tr>",
escape(&conv.to_string()),
state.cell()
)
}));
let conversions_ok = conversions.iter().filter(|(_, s)| s.ok()).count();
let page = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<meta name="description" content="tclrs — Command reference. The {count} commands the current tclrs build compiles, their ensembles, and the expr operator ladder, generated from the compiler's own tables. MIT licensed.">
<title>tclrs — Command Reference</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="hud-static.css">
<link rel="stylesheet" href="tutorial.css">
<style>
.tutorial-main {{ max-width: 76rem; }}
.file-table {{ width:100%;border-collapse:collapse;margin:0.6rem 0;font-size:12px; }}
.file-table th {{ background:var(--bg-secondary);color:var(--cyan);font-family:'Orbitron',sans-serif;font-size:10px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;text-align:left;padding:7px 10px;border:1px solid var(--border); }}
.file-table td {{ padding:6px 10px;border:1px solid var(--border);color:var(--text-dim);vertical-align:middle; }}
.file-table tr:hover td {{ background:var(--bg-hover); }}
.file-table td:first-child {{ font-family:'Share Tech Mono',monospace;color:var(--accent-light);font-weight:600;white-space:nowrap; }}
.file-table code {{ font-size:11px;color:var(--accent-light);background:var(--bg-primary);padding:1px 4px;border-radius:2px; }}
.stat-grid {{ display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:0.75rem;margin:1.2rem 0; }}
.stat-card {{ border:1px solid var(--border);border-top:3px solid var(--cyan);background:var(--bg-card);padding:1rem 1.2rem;border-radius:2px;text-align:center; }}
.stat-card .stat-val {{ font-family:'Orbitron',sans-serif;font-size:28px;font-weight:900;color:var(--cyan);line-height:1.1;text-shadow:0 0 20px var(--cyan-glow); }}
.stat-card .stat-val.accent {{ color:var(--accent);text-shadow:0 0 20px var(--accent-glow); }}
.stat-card .stat-label {{ font-family:'Orbitron',sans-serif;font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--text-muted);margin-top:0.5rem; }}
.docs-build-line {{ margin:0.35rem 0 0;font-family:'Share Tech Mono',ui-monospace,monospace;font-size:11px;color:var(--text-dim);letter-spacing:0.03em;max-width:42rem;opacity:0.75; }}
.state-yes {{ color:var(--green,#39ff14);font-weight:600; }}
.state-no {{ color:var(--accent,#ff2a6d);font-weight:600; }}
</style>
</head>
<body>
<div class="app tutorial-app" id="docsApp">
<div class="crt-scanline" id="crtH" aria-hidden="true"></div>
<div class="crt-scanline-v" id="crtV" aria-hidden="true"></div>
<header class="tutorial-header">
<div class="tutorial-header-inner">
<div>
<h1 class="tutorial-brand">// TCLRS — COMMAND REFERENCE</h1>
<nav class="tutorial-crumbs" aria-label="Breadcrumb">
<a href="index.html">Docs</a>
<span class="sep">/</span>
<a href="report.html">Engineering Report</a>
<span class="sep">/</span>
<span class="current">Command Reference</span>
<span class="sep">/</span>
<a href="https://github.com/MenkeTechnologies/tclrs" target="_blank" rel="noopener noreferrer">GitHub</a>
</nav>
<p class="docs-build-line">tclrs v{version} · generated from the compiler's own tables · MIT</p>
</div>
<div class="tutorial-toolbar">
<button type="button" class="btn btn-secondary" id="btnTheme" title="Toggle light/dark">Theme</button>
<button type="button" class="btn btn-secondary active" id="btnCrt" title="CRT scanline overlay">CRT</button>
<button type="button" class="btn btn-secondary active" id="btnNeon" title="Neon border pulse">Neon</button>
<a class="btn btn-secondary" href="index.html">Docs</a>
<a class="btn btn-secondary" href="report.html">Report</a>
<a class="btn btn-secondary" href="https://github.com/MenkeTechnologies/tclrs" target="_blank" rel="noopener noreferrer">GitHub</a>
</div>
</div>
</header>
<main class="tutorial-main">
<section id="commands">
<h2>Commands</h2>
<p>Every command the current build compiles. A name absent from this
table is <code>invalid command name "…"</code> at <em>compile</em>
time, not a runtime lookup that fails — resolving the name while
compiling is what turns a call into a <code>Op::Call</code> to a known
entry. The list is the compiler's own: the names it matches itself, plus
the list commands it forwards.</p>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-val">{count}</div>
<div class="stat-label">Commands</div>
</div>
<div class="stat-card">
<div class="stat-val">{sub_ok}/{sub_total}</div>
<div class="stat-label">Ensemble subcommands</div>
</div>
<div class="stat-card">
<div class="stat-val">{operator_count}</div>
<div class="stat-label">expr binary operators</div>
</div>
<div class="stat-card">
<div class="stat-val accent">v{version}</div>
<div class="stat-label">Build</div>
</div>
</div>
<table class="file-table">
<colgroup><col style="width:14%"><col style="width:29%"><col style="width:57%"></colgroup>
<thead><tr><th>command</th><th>synopsis</th><th>what it does</th></tr></thead>
<tbody>
{command_rows} </tbody>
</table>
</section>
<section id="ensembles">
<h2>Ensemble subcommands</h2>
<p>An ensemble resolves its subcommand the way
<code>Tcl_GetIndexFromObj</code> does — an exact match, else a prefix
that fits exactly one entry — so the tables below carry the names the
frontend <em>recognises</em>, which is what decides whether an
abbreviation is ambiguous. Recognising a name is not implementing it:
the second column is the answer the compiler actually gives when the
subcommand is put through it.</p>
{ensemble_tables} </section>
<section id="expr">
<h2>The <code>expr</code> operator ladder</h2>
<p>Precedence levels, tightest last, printed from the table the parser
binds with. Unary <code>+ - ~ !</code> bind tighter than every level
below, and the ternary <code>?:</code> looser; <code>**</code> is the
one right-associative level. The string comparisons share a level with
their numeric counterparts, as <code>expr(n)</code> specifies — so
<code>"a" eq "a" == 1</code> is 1.</p>
<table class="file-table">
<colgroup><col style="width:10%"><col style="width:60%"><col style="width:30%"></colgroup>
<thead><tr><th>level</th><th>operators</th><th>associativity</th></tr></thead>
<tbody>
{operator_rows} </tbody>
</table>
</section>
<section id="classes">
<h2><code>string is</code> classes</h2>
<p>Resolved while compiling, in the interpreter's own listing order. The
classes that need Tcl's Unicode general-category tables accept ASCII and
report an error otherwise, rather than answering from a different
Unicode revision than the reference interpreter's.</p>
<p>{class_cells}</p>
</section>
<section id="format">
<h2><code>format</code> conversions</h2>
<p>{conversions_ok} conversions, established by running one of each
through the runtime and reading the answer. A conversion missing from
this table is <code>bad field specifier</code>.</p>
<table class="file-table">
<colgroup><col style="width:20%"><col style="width:80%"></colgroup>
<thead><tr><th>conversion</th><th>state</th></tr></thead>
<tbody>
{conversion_rows} </tbody>
</table>
</section>
</main>
</div>
<script src="hud-theme.js"></script>
</body>
</html>
"#
);
let out = "docs/reference.html";
if let Err(e) = std::fs::write(out, page) {
eprintln!("gen-docs: cannot write {out}: {e}");
std::process::exit(1);
}
println!("wrote {out} ({count} commands, {sub_total} subcommands, {operator_count} operators)");
}
enum State {
Implemented,
Refused(String),
}
impl State {
fn ok(&self) -> bool {
matches!(self, State::Implemented)
}
fn cell(&self) -> String {
match self {
State::Implemented => "<span class=\"state-yes\">implemented</span>".into(),
State::Refused(msg) => format!("<span class=\"state-no\">{}</span>", escape(msg)),
}
}
}
struct Ensemble {
name: &'static str,
subs: Vec<(&'static str, State)>,
}
impl Ensemble {
fn probe(name: &'static str) -> Ensemble {
let subs = names::subcommands(name)
.iter()
.map(|sub| {
let mut refusal = None;
for argc in 0..=4 {
let args = ["a", "b", "c", "d"][..argc].join(" ");
match tclrs::runtime::compile(&format!("{name} {sub} {args}")) {
Ok(_) => return (*sub, State::Implemented),
Err(e) if is_refusal(&e) => refusal = Some(strip_line(&e)),
Err(_) => return (*sub, State::Implemented),
}
}
match refusal {
Some(msg) => (*sub, State::Refused(msg)),
None => (*sub, State::Implemented),
}
})
.collect();
Ensemble { name, subs }
}
fn implemented(&self) -> usize {
self.subs.iter().filter(|(_, s)| s.ok()).count()
}
fn table(&self) -> String {
let body = rows(self.subs.iter().map(|(sub, state)| {
format!(
"<tr><td><code>{} {}</code></td><td>{}</td></tr>",
escape(self.name),
escape(sub),
state.cell()
)
}));
format!(
" <h3><code>{name}</code> — {ok} of {all} implemented</h3>\n \
<table class=\"file-table\">\n \
<colgroup><col style=\"width:32%\"><col style=\"width:68%\"></colgroup>\n \
<thead><tr><th>subcommand</th><th>state</th></tr></thead>\n <tbody>\n\
{body} </tbody>\n </table>\n",
name = escape(self.name),
ok = self.implemented(),
all = self.subs.len(),
)
}
}
fn conversions() -> Vec<(char, State)> {
let mut found = Vec::new();
for conv in ('a'..='z').chain('A'..='Z') {
let script = format!("format %{conv} 1");
match tclrs::eval(&script) {
Ok(_) => found.push((conv, State::Implemented)),
Err(e) if is_refusal(&e.to_string()) => {
found.push((conv, State::Refused(strip_line(&e.to_string()))))
}
Err(e) if e.to_string().contains("bad field specifier") => {}
Err(_) => found.push((conv, State::Implemented)),
}
}
found
}
fn is_refusal(msg: &str) -> bool {
msg.contains("is not supported yet")
}
fn strip_line(msg: &str) -> String {
match msg.find(" (line ") {
Some(at) => msg[..at].to_string(),
None => msg.to_string(),
}
}
fn rows(cells: impl Iterator<Item = String>) -> String {
let mut out = String::new();
for cell in cells {
let _ = writeln!(out, " {cell}");
}
out
}
fn escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}