Skip to content

Commit 53bd6ad

Browse files
committed
add kotlin parser and docgen for generating man-pages
1 parent b2c89aa commit 53bd6ad

File tree

4 files changed

+321
-157
lines changed

4 files changed

+321
-157
lines changed

src/doc/DocGen.kt

Lines changed: 94 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,123 @@
1+
package doc
2+
3+
import util.Result
14
import java.io.File
2-
import java.io.PrintWriter
5+
import kotlin.system.exitProcess
6+
7+
data class KDocData(val filename: String, val parsed: ParsedFile)
38

4-
/**
5-
* A simple program to generate man pages from Kotlin source files.
6-
* This script processes multiple files, generating a summary man page
7-
* and a separate man page for each class found.
8-
*/
9+
/** Program to generate man pages for provided kotlin files */
910
fun main(args: Array<String>) {
1011
if (args.isEmpty()) {
1112
println("Usage: ./nob doc <file.kt> <file2.kt> ...")
12-
return
13-
}
14-
15-
val man1_dir = File("man1")
16-
if (!man1_dir.exists()) {
17-
man1_dir.mkdir()
13+
exitProcess(1)
1814
}
1915

20-
val all_files_data = mutableMapOf<String, ManPageData>()
21-
val entrypoint_data = mutableMapOf<String, String>()
22-
23-
val class_pattern = """(?:/\*\*(.*?)\*/\s*)?(data\s+)?(class|object)\s+([a-zA-Z0-9_]+)""".toRegex(RegexOption.DOT_MATCHES_ALL)
24-
val fun_pattern = """^\s*(?:/\*\*\s*\n(.*?)\s*\*/\s*)?fun\s+([a-zA-Z0-9_]+)(?:\s*<.*?>)?\((.*?)\)(?:\s*:\s*[a-zA-Z0-9_<>]+)?""".toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE))
25-
val property_pattern = """^\s*(?:/\*\*\s*\n(.*?)\s*\*/\s*)?(const\s+)?(val|var)\s+([a-zA-Z0-9_]+)\s*:\s*([a-zA-Z0-9_<>]+)""".toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE))
26-
27-
for (file_path in args) {
28-
val src_file = File(file_path)
29-
if (!src_file.exists()) {
30-
println("Error: File not found at ${src_file.absolutePath}")
31-
continue
32-
}
33-
34-
val source_code = src_file.readText()
35-
val package_name = source_code.lineSequence().firstOrNull { it.startsWith("package ") }?.removePrefix("package ")?.trim() ?: ""
36-
val file_data = ManPageData(package_name, src_file.name)
37-
38-
class_pattern.findAll(source_code).forEach { match ->
39-
val (comment_group, is_data, type, class_name) = match.destructured
40-
val comment = comment_group ?: "No description available."
41-
file_data.classes.add(class_name)
42-
file_data.class_data[class_name] = ClassData(
43-
comment = clean_kdoc_comment(comment),
44-
is_data = is_data.isNotBlank(),
45-
type = type
46-
)
16+
val files = args.map { File(it) }.filter { it.exists() }
17+
val fileDataList = mutableListOf<KDocData>()
18+
19+
for (file in files) {
20+
val source = file.readText()
21+
when (val parsed = KotlinParser.parse(source)) {
22+
is Result.Ok -> {
23+
fileDataList.add(KDocData(
24+
filename = file.nameWithoutExtension,
25+
parsed = parsed.value.result
26+
))
27+
}
28+
is Result.Err -> {
29+
val err = parsed.error
30+
println("Failed to parse ${file.name}: ${err.reason}")
31+
println(" At line ${err.line}, column ${err.col}")
32+
}
4733
}
34+
}
4835

49-
fun_pattern.findAll(source_code).forEach { match ->
50-
val (comment, signature, _, _) = match.destructured
51-
file_data.functions.add(signature)
36+
generate_summary_page(fileDataList)
37+
fileDataList
38+
.filterNot { it.parsed.functions.any { it.name == "main" } }
39+
.forEach(::generate_class_page)
40+
}
5241

53-
if (signature.substringBefore('<') == "main") {
54-
entrypoint_data["name"] = if (file_data.package_name.isNotBlank()) "${file_data.package_name}.$signature" else signature
55-
entrypoint_data["file"] = src_file.name
56-
entrypoint_data["kdoc"] = clean_kdoc_comment(comment ?: "No description available.")
42+
/** Generates a summary named after the file containing main() */
43+
fun generate_summary_page(files: List<KDocData>) {
44+
val main_file = files.firstOrNull { it.parsed.functions.any { f -> f.name == "main" } } ?: return
45+
val sb = StringBuilder()
46+
sb.appendLine(".TH SUMMARY 1")
47+
sb.appendLine(".SH DESCRIPTION")
48+
sb.appendLine(main_file.parsed.functions.firstOrNull { it.name == "main" }?.kdoc ?: "No description available.")
49+
50+
sb.appendLine()
51+
sb.appendLine(".SH CLASSES")
52+
files.forEach { f ->
53+
f.parsed.classes.forEach { c ->
54+
sb.appendLine(".B ${c.name}(1)")
55+
if (!c.kdoc.isNullOrBlank()) {
56+
sb.appendLine(" ${c.kdoc}")
57+
sb.appendLine(".br")
5758
}
5859
}
60+
}
5961

60-
property_pattern.findAll(source_code).forEach { match ->
61-
val (comment, is_const, _, property_name) = match.destructured
62-
file_data.globals.add(property_name)
63-
file_data.global_data[property_name] = GlobalData(
64-
comment = clean_kdoc_comment(comment ?: "No description available."),
65-
is_const = is_const.isNotBlank()
66-
)
62+
sb.appendLine(".SH FUNCTIONS")
63+
files.forEach { f ->
64+
f.parsed.functions.forEach { fn ->
65+
sb.appendLine(".B ${fn.name}(1)")
66+
fn.kdoc?.let { sb.appendLine(" $it")}
67+
sb.appendLine(".br")
6768
}
68-
all_files_data[file_path] = file_data
6969
}
7070

71-
generate_summary_page(all_files_data, entrypoint_data)
72-
73-
for ((_, file_data) in all_files_data) {
74-
for ((class_name, class_data) in file_data.class_data) {
75-
generate_class_page(class_name, class_data, file_data, all_files_data, entrypoint_data)
71+
sb.appendLine(".SH GLOBALS")
72+
files.forEach { f ->
73+
f.parsed.globals.forEach { g ->
74+
sb.appendLine("${g.name} = ${g.value}")
75+
g.kdoc?.let { sb.appendLine(" $it")}
76+
sb.appendLine(".br")
7677
}
7778
}
7879

79-
println("All man pages successfully generated.")
80-
}
81-
82-
/** Data class to hold parsed information for a single file. */
83-
data class ManPageData(
84-
val package_name: String,
85-
val file_name: String,
86-
val classes: MutableList<String> = mutableListOf(),
87-
val functions: MutableList<String> = mutableListOf(),
88-
val globals: MutableList<String> = mutableListOf(),
89-
val class_data: MutableMap<String, ClassData> = mutableMapOf(),
90-
val global_data: MutableMap<String, GlobalData> = mutableMapOf()
91-
)
92-
93-
/** Store specific details about each class */
94-
data class ClassData(val comment: String, val is_data: Boolean, val type: String)
95-
data class GlobalData(val comment: String, val is_const: Boolean)
96-
97-
/**
98-
* Cleans a KDoc comment by removing leading asterisks and trimming whitespace.
99-
*/
100-
fun clean_kdoc_comment(comment: String): String {
101-
return comment.lines().joinToString("\n") { line ->
102-
line.trimStart().removePrefix("*").trim()
103-
}.trim()
80+
File("man1/${main_file.filename.lowercase()}.1").apply {
81+
parentFile.mkdirs()
82+
writeText(sb.toString())
83+
}.also {
84+
println("generated $it")
85+
}
10486
}
10587

106-
fun generate_summary_page(all_files_data: MutableMap<String, ManPageData>, entrypoint_data: MutableMap<String, String>) {
107-
File("man1", "summary.1").printWriter().use { writer ->
108-
writer.println(".TH SUMMARY 1")
109-
writer.println(".SH NAME")
110-
writer.println("Summary \\- Overview of the Kotlin project.")
111-
writer.println(".SH DESCRIPTION")
112-
val main_kdoc = entrypoint_data["kdoc"]
113-
writer.println(main_kdoc?.replace("\n", "\n.br\n") ?: "No description available.")
114-
writer.println()
115-
writer.println(".SH ENTRYPOINT")
116-
val main_name = entrypoint_data["name"] ?: "No entry point found."
117-
val main_file = entrypoint_data["file"] ?: "N/A"
118-
writer.println(".B $main_name")
119-
writer.println("located in file .I $main_file")
120-
121-
122-
writer.println(".SH CLASSES")
123-
for ((_, file_data) in all_files_data) {
124-
file_data.class_data.forEach { (class_name, class_data) ->
125-
writer.println(".B ${class_name.lowercase()}(1)")
126-
writer.println(" ${get_first_sentence(class_data.comment)}")
127-
writer.println(".br")
128-
}
129-
}
130-
131-
writer.println(".SH FUNCTIONS")
132-
for ((_, file_data) in all_files_data) {
133-
file_data.functions.forEach { fun_signature ->
134-
writer.println(".B ${fun_signature.lowercase()}(1)")
135-
writer.println(".br")
136-
}
137-
}
88+
/** Generates one page per file */
89+
fun generate_class_page(file: KDocData) {
90+
val sb = StringBuilder()
91+
sb.appendLine(".TH ${file.filename.uppercase()} 1")
92+
sb.appendLine(".SH NAME")
93+
sb.appendLine("${file.filename} \\- ${file.parsed.functions.firstOrNull{ it.name == "main" }?.kdoc ?: "No description available"}")
94+
sb.appendLine(".SH SYNOPSIS")
95+
sb.appendLine("${file.filename.lowercase()}.${file.filename}")
96+
sb.appendLine()
97+
98+
sb.appendLine(".SH CLASSES")
99+
file.parsed.classes.forEach { c ->
100+
sb.appendLine(".B ${c.name}(1)")
101+
if (!c.kdoc.isNullOrBlank()) sb.appendLine(" ${c.kdoc}").appendLine(".br")
102+
}
138103

139-
writer.println(".SH GLOBALS")
140-
for ((_, file_data) in all_files_data) {
141-
file_data.globals.forEach { global_name ->
142-
writer.println(".B ${global_name.lowercase()}(1)")
143-
writer.println(".br")
144-
}
145-
}
104+
sb.appendLine(".SH FUNCTIONS")
105+
file.parsed.functions.forEach { fn ->
106+
sb.appendLine(".B ${fn.name}(1)")
107+
fn.kdoc?.let { sb.appendLine(" $it").appendLine(".br") }
146108
}
147-
}
148109

149-
fun generate_class_page(class_name: String, class_data: ClassData, file_data: ManPageData, all_files_data: MutableMap<String, ManPageData>, entrypoint_data: MutableMap<String, String>) {
150-
val output_filename = "${class_name.lowercase()}.1"
151-
File("man1", output_filename).printWriter().use { writer ->
152-
writer.println(".TH ${class_name.uppercase()} 1")
153-
writer.println(".SH NAME")
154-
writer.println("$class_name \\- ${get_first_sentence(class_data.comment)}")
155-
writer.println(".SH SYNOPSIS")
156-
writer.println("${file_data.package_name}.$class_name")
157-
writer.println(".SH DESCRIPTION")
158-
writer.println(class_data.comment.replace("\n", "\n.br\n"))
159-
writer.println()
160-
writer.println(".SH FUNCTIONS")
161-
file_data.functions.forEach { fun_signature ->
162-
writer.println(".B ${fun_signature.lowercase()}(1)")
163-
writer.println(".br")
164-
}
165-
writer.println(".SH GLOBALS")
166-
file_data.globals.forEach { global_name ->
167-
writer.println(".B ${global_name.lowercase()}(1)")
168-
writer.println(".br")
169-
}
110+
sb.appendLine(".SH GLOBALS")
111+
file.parsed.globals.forEach { g ->
112+
sb.appendLine(".B ${g.name}(1)")
113+
g.kdoc?.let { sb.appendLine(" $it").appendLine(".br") }
170114
}
171-
}
172115

173-
/** Extracts the first sentence from a KDoc comment. */
174-
fun get_first_sentence(comment: String): String {
175-
val period_idx = comment.indexOf('.')
176-
return if (period_idx != -1) {
177-
comment.substring(0, period_idx + 1).trim().replace("\n", " ")
178-
} else {
179-
comment.trim().replace("\n", " ")
116+
File("man1/${file.filename.lowercase()}.1").apply {
117+
parentFile.mkdirs()
118+
writeText(sb.toString())
119+
}.also {
120+
println("generated $it")
180121
}
181122
}
182123

0 commit comments

Comments
 (0)