|
| 1 | +package doc |
| 2 | + |
| 3 | +import util.Result |
1 | 4 | 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) |
3 | 8 |
|
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 */ |
9 | 10 | fun main(args: Array<String>) { |
10 | 11 | if (args.isEmpty()) { |
11 | 12 | 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) |
18 | 14 | } |
19 | 15 |
|
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 | + } |
47 | 33 | } |
| 34 | + } |
48 | 35 |
|
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 | +} |
52 | 41 |
|
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") |
57 | 58 | } |
58 | 59 | } |
| 60 | + } |
59 | 61 |
|
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") |
67 | 68 | } |
68 | | - all_files_data[file_path] = file_data |
69 | 69 | } |
70 | 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) |
| 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") |
76 | 77 | } |
77 | 78 | } |
78 | 79 |
|
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 | + } |
104 | 86 | } |
105 | 87 |
|
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 | + } |
138 | 103 |
|
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") } |
146 | 108 | } |
147 | | -} |
148 | 109 |
|
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") } |
170 | 114 | } |
171 | | -} |
172 | 115 |
|
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") |
180 | 121 | } |
181 | 122 | } |
182 | 123 |
|
0 commit comments