Files
dimgigov 53b43b0f79 feat: lifetime elision, tooling CI, registry, and LSP locals
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1
lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks,
stdlib goldens, package registry (bux search/add), and LSP 0.4
position-sensitive locals with inferred let types. Full-tree format
pass plus Map/Set remove double-free fix.
2026-07-19 16:35:08 +03:00

203 lines
5.7 KiB
Nim

## docgen.nim — Extract `///` (and adjacent `/* */`) docs into Markdown.
## Used by `bux doc [path...]`.
import std/[os, strutils, strformat, algorithm]
type
DocItem* = object
kind*: string ## module | func | struct | enum | interface | extern
name*: string
signature*: string ## first declaration line (trimmed)
docs*: string
file*: string
line*: int
proc isDeclStart(line: string): bool =
let s = line.strip()
if s.len == 0: return false
# Leading attributes @[Checked] etc. — not a decl by themselves
if s.startsWith("@["): return false
if s.startsWith("func ") or s.startsWith("pub func ") or
s.startsWith("extern func ") or s.startsWith("const func ") or
s.startsWith("async func "):
return true
if s.startsWith("struct ") or s.startsWith("pub struct ") or
s.startsWith("enum ") or s.startsWith("pub enum ") or
s.startsWith("union ") or s.startsWith("interface ") or
s.startsWith("module ") or s.startsWith("type "):
return true
return false
proc declKindAndName(line: string): tuple[kind, name: string] =
var s = line.strip()
# Strip leading pub/extern/const/async
for prefix in ["pub ", "extern ", "const ", "async "]:
if s.startsWith(prefix):
s = s[prefix.len .. ^1].strip()
var kind = "item"
if s.startsWith("func "):
kind = "func"
s = s["func ".len .. ^1]
elif s.startsWith("struct "):
kind = "struct"
s = s["struct ".len .. ^1]
elif s.startsWith("enum "):
kind = "enum"
s = s["enum ".len .. ^1]
elif s.startsWith("union "):
kind = "union"
s = s["union ".len .. ^1]
elif s.startsWith("interface "):
kind = "interface"
s = s["interface ".len .. ^1]
elif s.startsWith("module "):
kind = "module"
s = s["module ".len .. ^1]
elif s.startsWith("type "):
kind = "type"
s = s["type ".len .. ^1]
# Name: until `<` `(` `{` `:` space
var name = ""
for ch in s:
if ch in {' ', '<', '(', '{', ':', ';'}:
break
name.add(ch)
if name.len == 0:
name = s
# extern funcs already stripped "extern "
if kind == "func" and line.strip().startsWith("extern"):
kind = "extern"
return (kind, name)
proc extractDocsFromSource*(source, path: string): seq[DocItem] =
result = @[]
var pending: seq[string] = @[]
var inBlockComment = false
var blockDoc: seq[string] = @[]
var lineNo = 0
for rawLine in source.splitLines():
inc lineNo
var line = rawLine
let stripped = line.strip()
# Block comment handling (/* ... */ used in stdlib today)
if inBlockComment:
let endIdx = stripped.find("*/")
if endIdx >= 0:
let before = stripped[0 ..< endIdx].strip()
if before.len > 0:
blockDoc.add(before)
inBlockComment = false
# Treat completed block as pending doc if non-empty
if blockDoc.len > 0:
pending = blockDoc
blockDoc = @[]
continue
else:
blockDoc.add(stripped)
continue
if stripped.startsWith("/*") and not stripped.startsWith("/***"):
let rest = stripped["/*".len .. ^1]
let endIdx = rest.find("*/")
if endIdx >= 0:
let body = rest[0 ..< endIdx].strip()
if body.len > 0:
pending = @[body]
else:
inBlockComment = true
blockDoc = @[]
let body = rest.strip()
if body.len > 0:
blockDoc.add(body)
continue
# Triple-slash doc comments
if stripped.startsWith("///"):
var body = stripped["///".len .. ^1]
if body.startsWith(" "):
body = body[1 .. ^1]
pending.add(body)
continue
# Empty line: keep pending docs (allow blank lines inside doc blocks)
if stripped.len == 0:
continue
# Attributes immediately before decl: keep pending
if stripped.startsWith("@["):
continue
if isDeclStart(line):
if pending.len > 0:
let (kind, name) = declKindAndName(line)
result.add(DocItem(
kind: kind,
name: name,
signature: stripped,
docs: pending.join("\n"),
file: path,
line: lineNo
))
pending = @[]
continue
# Other code clears pending (except plain // comments)
if stripped.startsWith("//"):
continue
pending = @[]
proc collectBuxFilesForDoc*(root: string): seq[string] =
result = @[]
if fileExists(root) and root.endsWith(".bux"):
result.add(root)
return
if not dirExists(root):
return
for path in walkDirRec(root):
if path.endsWith(".bux"):
result.add(path)
result.sort(system.cmp)
proc renderMarkdown*(items: seq[DocItem], title: string = "API Reference"): string =
var sb: string
sb.add(&"# {title}\n\n")
sb.add("Generated by `bux doc` from `///` and `/* */` documentation comments.\n\n")
if items.len == 0:
sb.add("_No documented items found._\n")
return sb
# Group by file
var byFile: seq[string] = @[]
for it in items:
if it.file notin byFile:
byFile.add(it.file)
byFile.sort(system.cmp)
for f in byFile:
let base = splitFile(f).name
sb.add(&"## `{base}`\n\n")
sb.add(&"_Source: `{f}`_\n\n")
for it in items:
if it.file != f:
continue
sb.add(&"### `{it.name}` _{it.kind}_\n\n")
sb.add("```bux\n")
sb.add(it.signature)
sb.add("\n```\n\n")
if it.docs.len > 0:
sb.add(it.docs)
sb.add("\n\n")
return sb
proc generateDocs*(paths: seq[string]): seq[DocItem] =
result = @[]
var files: seq[string] = @[]
for p in paths:
for f in collectBuxFilesForDoc(p):
if f notin files:
files.add(f)
files.sort(system.cmp)
for f in files:
let src = readFile(f)
result.add(extractDocsFromSource(src, f))