|
| 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