Skip to content

Commit b2c89aa

Browse files
committed
first versino of a man page generator
1 parent ab36c30 commit b2c89aa

3 files changed

Lines changed: 212 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.build
22
/out
3+
/man1
34
.libs
45
kls-classpath

nob.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ fun main(args: Array<String>) {
3838
nob.compile(opts.test_dir.toFile())
3939
nob.run_test(args)
4040
}
41+
opts.doc -> nob.run_doc(args)
4142
opts.run -> nob.run_target()
4243
else -> 0
4344
}
@@ -81,6 +82,25 @@ class Nob(private val opts: Opts) {
8182
)
8283
}
8384

85+
fun run_doc(args: Array<String>): Int {
86+
val main = opts.main_srcs.first { it.toFile().name == "DocGen.kt" }
87+
debug("Generating doc $main")
88+
return exec(
89+
buildList {
90+
add("java")
91+
add("-Dfile.encoding=UTF-8")
92+
add("-Dsun.stdout.encoding=UTF-8")
93+
add("-Dsun.stderr.encoding=UTF-8")
94+
if (opts.debugger) add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")
95+
add("-cp")
96+
add(opts.test_classpath())
97+
add(opts.main_class(main.toFile()))
98+
args.drop(2).forEach { add(it) }
99+
},
100+
opts
101+
)
102+
}
103+
84104
fun run_target(): Int {
85105
debug("Running ${opts.main_src}")
86106
return exec(
@@ -204,6 +224,7 @@ private fun parse_args(args: Array<String>): Opts {
204224
when (val arg = args.getOrNull(pos++)) {
205225
"debug" -> opts.debugger = true
206226
"test" -> opts.test = true
227+
"doc" -> opts.doc = true
207228
null -> break
208229
}
209230
}
@@ -230,13 +251,19 @@ data class Opts(
230251
var debugger: Boolean = false,
231252
var run: Boolean = false,
232253
var test: Boolean = false,
254+
var doc: Boolean = false,
233255
) {
234256
val main_src: Path get() = Files.walk(src_dir)
235257
.filter { it.toFile().isFile() }
236258
.filter { it.toFile().readText().contains("fun main(") }
237259
.toList()
238260
.firstOrNull() ?: error("no main() found")
239261

262+
val main_srcs: List<Path> get() = Files.walk(src_dir)
263+
.filter { it.toFile().isFile() }
264+
.filter { it.toFile().readText().contains("fun main(") }
265+
.toList()
266+
240267
fun runtime_classpath(): String {
241268
val libs_paths = libs.filter { it.scope == "compile" || it.scope == "runtime" }
242269
.joinToString(File.pathSeparator) { it.jar_path.toAbsolutePath().normalize().toString() }
@@ -272,7 +299,8 @@ data class Opts(
272299

273300
fun main_class(src: File): String {
274301
val pkg = src.useLines { lines -> lines.firstOrNull { it.trim().startsWith("package ") }?.removePrefix("package ")?.trim() }
275-
val main = src.toPath().fileName.toString().removeSuffix(".kt").lowercase().replaceFirstChar{ it.uppercase() } + "Kt"
302+
// val main = src.toPath().fileName.toString().removeSuffix(".kt").lowercase().replaceFirstChar{ it.uppercase() } + "Kt"
303+
val main = src.toPath().fileName.toString().removeSuffix(".kt").replaceFirstChar{ it.uppercase() } + "Kt"
276304
return when (pkg) {
277305
null -> "$main"
278306
else -> "$pkg.$main"

src/doc/DocGen.kt

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import java.io.File
2+
import java.io.PrintWriter
3+
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+
fun main(args: Array<String>) {
10+
if (args.isEmpty()) {
11+
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()
18+
}
19+
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+
)
47+
}
48+
49+
fun_pattern.findAll(source_code).forEach { match ->
50+
val (comment, signature, _, _) = match.destructured
51+
file_data.functions.add(signature)
52+
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.")
57+
}
58+
}
59+
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+
)
67+
}
68+
all_files_data[file_path] = file_data
69+
}
70+
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)
76+
}
77+
}
78+
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()
104+
}
105+
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+
}
138+
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+
}
146+
}
147+
}
148+
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+
}
170+
}
171+
}
172+
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", " ")
180+
}
181+
}
182+

0 commit comments

Comments
 (0)