blob: 2a4d9038f3f00a95d6e9befd5a6371029a9c37e3 [file] [log] [blame]
Roman Elizarovf16fd272017-02-07 11:26:00 +03001/*
Roman Elizarov1f74a2d2018-06-29 19:19:45 +03002 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
Roman Elizarovf16fd272017-02-07 11:26:00 +03003 */
4
Roman Elizarove7e2ad12017-05-17 14:47:31 +03005import java.io.File
6import java.io.IOException
7import java.io.LineNumberReader
8import java.io.Reader
9import java.util.*
10import kotlin.properties.Delegates
11
12// --- props in knit.properties
13
14val knitProperties = ClassLoader.getSystemClassLoader()
15 .getResource("knit.properties").openStream().use { Properties().apply { load(it) } }
16
17val siteRoot = knitProperties.getProperty("site.root")!!
18val moduleRoots = knitProperties.getProperty("module.roots").split(" ")
19val moduleMarker = knitProperties.getProperty("module.marker")!!
20val moduleDocs = knitProperties.getProperty("module.docs")!!
21
22// --- markdown syntax
Roman Elizarovb3d55a52017-02-03 12:47:21 +030023
Roman Elizarov731f0ad2017-02-22 20:48:45 +030024const val DIRECTIVE_START = "<!--- "
25const val DIRECTIVE_END = "-->"
Roman Elizarovb3d55a52017-02-03 12:47:21 +030026
Roman Elizarov731f0ad2017-02-22 20:48:45 +030027const val TOC_DIRECTIVE = "TOC"
28const val KNIT_DIRECTIVE = "KNIT"
29const val INCLUDE_DIRECTIVE = "INCLUDE"
30const val CLEAR_DIRECTIVE = "CLEAR"
31const val TEST_DIRECTIVE = "TEST"
Roman Elizarove0c817d2017-02-10 10:22:01 +030032
Roman Elizarov731f0ad2017-02-22 20:48:45 +030033const val TEST_OUT_DIRECTIVE = "TEST_OUT"
Roman Elizarovfa7723e2017-02-06 11:17:51 +030034
Roman Elizarove7e2ad12017-05-17 14:47:31 +030035const val MODULE_DIRECTIVE = "MODULE"
Roman Elizarov731f0ad2017-02-22 20:48:45 +030036const val INDEX_DIRECTIVE = "INDEX"
Roman Elizarovb3d55a52017-02-03 12:47:21 +030037
Roman Elizarov731f0ad2017-02-22 20:48:45 +030038const val CODE_START = "```kotlin"
39const val CODE_END = "```"
40
41const val TEST_START = "```text"
42const val TEST_END = "```"
43
44const val SECTION_START = "##"
45
46const val PACKAGE_PREFIX = "package "
47const val STARTS_WITH_PREDICATE = "STARTS_WITH"
Roman Elizarov1e459602017-02-27 11:05:17 +030048const val ARBITRARY_TIME_PREDICATE = "ARBITRARY_TIME"
Roman Elizarov731f0ad2017-02-22 20:48:45 +030049const val FLEXIBLE_TIME_PREDICATE = "FLEXIBLE_TIME"
50const val FLEXIBLE_THREAD_PREDICATE = "FLEXIBLE_THREAD"
51const val LINES_START_UNORDERED_PREDICATE = "LINES_START_UNORDERED"
52const val LINES_START_PREDICATE = "LINES_START"
Roman Elizarovfa7723e2017-02-06 11:17:51 +030053
Roman Elizarov88396732017-09-27 21:30:47 +030054val API_REF_REGEX = Regex("(^|[ \\]])\\[([A-Za-z0-9_().]+)\\]($|[^\\[\\(])")
Roman Elizarov419a6c82017-02-09 18:36:22 +030055
Roman Elizarovb3d55a52017-02-03 12:47:21 +030056fun main(args: Array<String>) {
Roman Elizarova5e653f2017-02-13 13:49:55 +030057 if (args.isEmpty()) {
58 println("Usage: Knit <markdown-files>")
Roman Elizarovb3d55a52017-02-03 12:47:21 +030059 return
60 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +030061 args.forEach {
62 if (!knit(it)) System.exit(1) // abort on first error with error exit code
63 }
Roman Elizarova5e653f2017-02-13 13:49:55 +030064}
65
Roman Elizarove7e2ad12017-05-17 14:47:31 +030066fun knit(markdownFileName: String): Boolean {
Roman Elizarova5e653f2017-02-13 13:49:55 +030067 println("*** Reading $markdownFileName")
68 val markdownFile = File(markdownFileName)
Roman Elizarov8a4a8e12017-03-09 19:52:58 +030069 val tocLines = arrayListOf<String>()
Roman Elizarovb3d55a52017-02-03 12:47:21 +030070 var knitRegex: Regex? = null
71 val includes = arrayListOf<Include>()
Roman Elizarov8a4a8e12017-03-09 19:52:58 +030072 val codeLines = arrayListOf<String>()
73 val testLines = arrayListOf<String>()
74 var testOut: String? = null
75 val testOutLines = arrayListOf<String>()
Roman Elizarov731f0ad2017-02-22 20:48:45 +030076 var lastPgk: String? = null
Roman Elizarov23f864e2017-03-03 19:57:47 +030077 val files = mutableSetOf<File>()
Roman Elizarov419a6c82017-02-09 18:36:22 +030078 val allApiRefs = arrayListOf<ApiRef>()
79 val remainingApiRefNames = mutableSetOf<String>()
Roman Elizarove7e2ad12017-05-17 14:47:31 +030080 var moduleName: String by Delegates.notNull()
81 var docsRoot: String by Delegates.notNull()
Roman Elizarovfa7723e2017-02-06 11:17:51 +030082 // read markdown file
Roman Elizarov419a6c82017-02-09 18:36:22 +030083 var putBackLine: String? = null
Roman Elizarovfa7723e2017-02-06 11:17:51 +030084 val markdown = markdownFile.withMarkdownTextReader {
Roman Elizarovb3d55a52017-02-03 12:47:21 +030085 mainLoop@ while (true) {
Roman Elizarov419a6c82017-02-09 18:36:22 +030086 val inLine = putBackLine ?: readLine() ?: break
87 putBackLine = null
Roman Elizarovfa7723e2017-02-06 11:17:51 +030088 val directive = directive(inLine)
89 if (directive != null && markdownPart == MarkdownPart.TOC) {
90 markdownPart = MarkdownPart.POST_TOC
91 postTocText += inLine
92 }
93 when (directive?.name) {
94 TOC_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +030095 requireSingleLine(directive)
Roman Elizarov731f0ad2017-02-22 20:48:45 +030096 require(directive.param.isEmpty()) { "$TOC_DIRECTIVE directive must not have parameters" }
Roman Elizarovfa7723e2017-02-06 11:17:51 +030097 require(markdownPart == MarkdownPart.PRE_TOC) { "Only one TOC directive is supported" }
98 markdownPart = MarkdownPart.TOC
99 }
100 KNIT_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300101 requireSingleLine(directive)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300102 require(!directive.param.isEmpty()) { "$KNIT_DIRECTIVE directive must include regex parameter" }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300103 require(knitRegex == null) { "Only one KNIT directive is supported"}
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300104 knitRegex = Regex("\\((" + directive.param + ")\\)")
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300105 continue@mainLoop
106 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300107 INCLUDE_DIRECTIVE -> {
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300108 if (directive.param.isEmpty()) {
109 require(!directive.singleLine) { "$INCLUDE_DIRECTIVE directive without parameters must not be single line" }
110 readUntilTo(DIRECTIVE_END, codeLines)
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300111 } else {
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300112 val include = Include(Regex(directive.param))
113 if (directive.singleLine) {
114 include.lines += codeLines
115 codeLines.clear()
116 } else {
117 readUntilTo(DIRECTIVE_END, include.lines)
118 }
119 includes += include
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300120 }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300121 continue@mainLoop
122 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300123 CLEAR_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300124 requireSingleLine(directive)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300125 require(directive.param.isEmpty()) { "$CLEAR_DIRECTIVE directive must not have parameters" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300126 codeLines.clear()
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300127 continue@mainLoop
128 }
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300129 TEST_OUT_DIRECTIVE -> {
130 require(!directive.param.isEmpty()) { "$TEST_OUT_DIRECTIVE directive must include file name parameter" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300131 flushTestOut(markdownFile.parentFile, testOut, testOutLines)
132 testOut = directive.param
133 readUntil(DIRECTIVE_END).forEach { testOutLines += it }
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300134 }
135 TEST_DIRECTIVE -> {
136 require(lastPgk != null) { "'$PACKAGE_PREFIX' prefix was not found in emitted code"}
137 require(testOut != null) { "$TEST_OUT_DIRECTIVE directive was not specified" }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300138 val predicate = directive.param
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300139 if (testLines.isEmpty()) {
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300140 if (directive.singleLine) {
141 require(!predicate.isEmpty()) { "$TEST_OUT_DIRECTIVE must be preceded by $TEST_START block or contain test predicate"}
142 } else
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300143 testLines += readUntil(DIRECTIVE_END)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300144 } else {
145 requireSingleLine(directive)
146 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300147 makeTest(testOutLines, lastPgk!!, testLines, predicate)
148 testLines.clear()
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300149 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300150 MODULE_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300151 requireSingleLine(directive)
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300152 moduleName = directive.param
153 docsRoot = findModuleRootDir(moduleName) + "/" + moduleDocs + "/" + moduleName
Roman Elizarove0c817d2017-02-10 10:22:01 +0300154 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300155 INDEX_DIRECTIVE -> {
156 requireSingleLine(directive)
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300157 val indexLines = processApiIndex(siteRoot + "/" + moduleName, docsRoot, directive.param, remainingApiRefNames)
158 ?: throw IllegalArgumentException("Failed to load index for ${directive.param}")
Roman Elizarov419a6c82017-02-09 18:36:22 +0300159 skip = true
160 while (true) {
161 val skipLine = readLine() ?: break@mainLoop
162 if (directive(skipLine) != null) {
163 putBackLine = skipLine
164 break
165 }
166 }
167 skip = false
Roman Elizarova5e653f2017-02-13 13:49:55 +0300168 outText += indexLines
169 outText += putBackLine!!
Roman Elizarov419a6c82017-02-09 18:36:22 +0300170 }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300171 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300172 if (inLine.startsWith(CODE_START)) {
Roman Elizarovf724f6e2017-04-07 18:06:22 +0300173 require(testOut == null || testLines.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300174 codeLines += ""
175 readUntilTo(CODE_END, codeLines)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300176 continue@mainLoop
177 }
178 if (inLine.startsWith(TEST_START)) {
Roman Elizarovf724f6e2017-04-07 18:06:22 +0300179 require(testOut == null || testLines.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300180 readUntilTo(TEST_END, testLines)
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300181 continue@mainLoop
182 }
183 if (inLine.startsWith(SECTION_START) && markdownPart == MarkdownPart.POST_TOC) {
184 val i = inLine.indexOf(' ')
185 require(i >= 2) { "Invalid section start" }
186 val name = inLine.substring(i + 1).trim()
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300187 tocLines += " ".repeat(i - 2) + "* [$name](#${makeSectionRef(name)})"
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300188 continue@mainLoop
189 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300190 for (match in API_REF_REGEX.findAll(inLine)) {
191 val apiRef = ApiRef(lineNumber, match.groups[2]!!.value)
192 allApiRefs += apiRef
193 remainingApiRefNames += apiRef.name
194 }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300195 knitRegex?.find(inLine)?.let { knitMatch ->
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300196 val fileName = knitMatch.groups[1]!!.value
Roman Elizarov23f864e2017-03-03 19:57:47 +0300197 val file = File(markdownFile.parentFile, fileName)
198 require(files.add(file)) { "Duplicate file: $file"}
199 println("Knitting $file ...")
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300200 val outLines = arrayListOf<String>()
201 for (include in includes) {
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300202 val includeMatch = include.regex.matchEntire(fileName) ?: continue
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300203 include.lines.forEach { includeLine ->
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300204 val line = makeReplacements(includeLine, includeMatch)
205 if (line.startsWith(PACKAGE_PREFIX))
206 lastPgk = line.substring(PACKAGE_PREFIX.length).trim()
207 outLines += line
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300208 }
209 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300210 outLines += codeLines
211 codeLines.clear()
212 writeLinesIfNeeded(file, outLines)
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300213 }
214 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300215 } ?: return false // false when failed
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300216 // update markdown file with toc
Roman Elizarova5e653f2017-02-13 13:49:55 +0300217 val newLines = buildList<String> {
218 addAll(markdown.preTocText)
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300219 if (!tocLines.isEmpty()) {
Roman Elizarova5e653f2017-02-13 13:49:55 +0300220 add("")
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300221 addAll(tocLines)
Roman Elizarova5e653f2017-02-13 13:49:55 +0300222 add("")
223 }
224 addAll(markdown.postTocText)
225 }
226 if (newLines != markdown.inText) writeLines(markdownFile, newLines)
Roman Elizarov419a6c82017-02-09 18:36:22 +0300227 // check apiRefs
228 for (apiRef in allApiRefs) {
229 if (apiRef.name in remainingApiRefNames) {
230 println("WARNING: $markdownFile: ${apiRef.line}: Broken reference to [${apiRef.name}]")
231 }
232 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300233 // write test output
234 flushTestOut(markdownFile.parentFile, testOut, testOutLines)
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300235 return true
Roman Elizarov419a6c82017-02-09 18:36:22 +0300236}
237
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300238fun makeTest(testOutLines: MutableList<String>, pgk: String, test: List<String>, predicate: String) {
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300239 val funName = buildString {
240 var cap = true
241 for (c in pgk) {
242 if (c == '.') {
243 cap = true
244 } else {
245 append(if (cap) c.toUpperCase() else c)
246 cap = false
247 }
248 }
249 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300250 testOutLines += ""
251 testOutLines += " @Test"
252 testOutLines += " fun test$funName() {"
Roman Elizarovba0c0042017-07-12 13:01:17 +0300253 val prefix = " test(\"$funName\") { $pgk.main(emptyArray()) }"
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300254 when (predicate) {
255 "" -> makeTestLines(testOutLines, prefix, "verifyLines", test)
256 STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStartWith", test)
257 ARBITRARY_TIME_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesArbitraryTime", test)
258 FLEXIBLE_TIME_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesFlexibleTime", test)
259 FLEXIBLE_THREAD_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesFlexibleThread", test)
260 LINES_START_UNORDERED_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStartUnordered", test)
261 LINES_START_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStart", test)
262 else -> {
263 testOutLines += prefix + ".also { lines ->"
264 testOutLines += " check($predicate)"
265 testOutLines += " }"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300266 }
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300267 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300268 testOutLines += " }"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300269}
270
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300271private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, method: String, test: List<String>) {
272 testOutLines += "$prefix.$method("
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300273 for ((index, testLine) in test.withIndex()) {
274 val commaOpt = if (index < test.size - 1) "," else ""
275 val escapedLine = testLine.replace("\"", "\\\"")
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300276 testOutLines += " \"$escapedLine\"$commaOpt"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300277 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300278 testOutLines += " )"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300279}
280
281private fun makeReplacements(line: String, match: MatchResult): String {
282 var result = line
283 for ((id, group) in match.groups.withIndex()) {
284 if (group != null)
285 result = result.replace("\$\$$id", group.value)
286 }
287 return result
288}
289
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300290private fun flushTestOut(parentDir: File?, testOut: String?, testOutLines: MutableList<String>) {
291 if (testOut == null) return
292 val file = File(parentDir, testOut)
293 testOutLines += "}"
294 writeLinesIfNeeded(file, testOutLines)
295 testOutLines.clear()
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300296}
297
298private fun MarkdownTextReader.readUntil(marker: String): List<String> =
299 arrayListOf<String>().also { readUntilTo(marker, it) }
300
301private fun MarkdownTextReader.readUntilTo(marker: String, list: MutableList<String>) {
302 while (true) {
303 val line = readLine() ?: break
304 if (line.startsWith(marker)) break
305 list += line
306 }
307}
308
Roman Elizarova5e653f2017-02-13 13:49:55 +0300309private inline fun <T> buildList(block: ArrayList<T>.() -> Unit): List<T> {
310 val result = arrayListOf<T>()
311 result.block()
312 return result
313}
314
Roman Elizarov419a6c82017-02-09 18:36:22 +0300315private fun requireSingleLine(directive: Directive) {
316 require(directive.singleLine) { "${directive.name} directive must end on the same line with '$DIRECTIVE_END'" }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300317}
318
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300319fun makeSectionRef(name: String): String = name.replace(' ', '-').replace(".", "").toLowerCase()
320
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300321class Include(val regex: Regex, val lines: MutableList<String> = arrayListOf())
322
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300323class Directive(
324 val name: String,
325 val param: String,
326 val singleLine: Boolean
327)
328
329fun directive(line: String): Directive? {
330 if (!line.startsWith(DIRECTIVE_START)) return null
331 var s = line.substring(DIRECTIVE_START.length).trim()
332 val singleLine = s.endsWith(DIRECTIVE_END)
333 if (singleLine) s = s.substring(0, s.length - DIRECTIVE_END.length)
334 val i = s.indexOf(' ')
335 val name = if (i < 0) s else s.substring(0, i)
336 val param = if (i < 0) "" else s.substring(i).trim()
337 return Directive(name, param, singleLine)
338}
339
Roman Elizarov419a6c82017-02-09 18:36:22 +0300340class ApiRef(val line: Int, val name: String)
341
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300342enum class MarkdownPart { PRE_TOC, TOC, POST_TOC }
343
344class MarkdownTextReader(r: Reader) : LineNumberReader(r) {
Roman Elizarova5e653f2017-02-13 13:49:55 +0300345 val inText = arrayListOf<String>()
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300346 val preTocText = arrayListOf<String>()
347 val postTocText = arrayListOf<String>()
348 var markdownPart: MarkdownPart = MarkdownPart.PRE_TOC
Roman Elizarov419a6c82017-02-09 18:36:22 +0300349 var skip = false
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300350
Roman Elizarova5e653f2017-02-13 13:49:55 +0300351 val outText: MutableList<String> get() = when (markdownPart) {
352 MarkdownPart.PRE_TOC -> preTocText
353 MarkdownPart.POST_TOC -> postTocText
354 else -> throw IllegalStateException("Wrong state: $markdownPart")
355 }
356
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300357 override fun readLine(): String? {
358 val line = super.readLine() ?: return null
Roman Elizarova5e653f2017-02-13 13:49:55 +0300359 inText += line
360 if (!skip && markdownPart != MarkdownPart.TOC)
361 outText += line
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300362 return line
363 }
364}
365
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300366fun <T : LineNumberReader> File.withLineNumberReader(factory: (Reader) -> T, block: T.() -> Unit): T? {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300367 val reader = factory(reader())
368 reader.use {
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300369 try {
370 it.block()
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300371 } catch (e: Exception) {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300372 println("ERROR: ${this@withLineNumberReader}: ${it.lineNumber}: ${e.message}")
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300373 return null
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300374 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300375 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300376 return reader
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300377}
378
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300379fun File.withMarkdownTextReader(block: MarkdownTextReader.() -> Unit): MarkdownTextReader? =
Roman Elizarov419a6c82017-02-09 18:36:22 +0300380 withLineNumberReader<MarkdownTextReader>(::MarkdownTextReader, block)
381
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300382fun writeLinesIfNeeded(file: File, outLines: List<String>) {
383 val oldLines = try {
384 file.readLines()
385 } catch (e: IOException) {
386 emptyList<String>()
387 }
388 if (outLines != oldLines) writeLines(file, outLines)
389}
390
Roman Elizarov419a6c82017-02-09 18:36:22 +0300391fun writeLines(file: File, lines: List<String>) {
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300392 println(" Writing $file ...")
393 file.parentFile?.mkdirs()
394 file.printWriter().use { out ->
395 lines.forEach { out.println(it) }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300396 }
397}
Roman Elizarov419a6c82017-02-09 18:36:22 +0300398
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300399fun findModuleRootDir(name: String): String =
400 moduleRoots
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300401 .map { "$it/$name" }
402 .firstOrNull { File("$it/$moduleMarker").exists() }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300403 ?: throw IllegalArgumentException("Module $name is not found in any of the module root dirs")
404
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300405data class ApiIndexKey(
406 val docsRoot: String,
407 val pkg: String
408)
409
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300410val apiIndexCache: MutableMap<ApiIndexKey, Map<String, List<String>>> = HashMap()
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300411
Roman Elizarov419a6c82017-02-09 18:36:22 +0300412val REF_LINE_REGEX = Regex("<a href=\"([a-z/.\\-]+)\">([a-zA-z.]+)</a>")
413val INDEX_HTML = "/index.html"
414val INDEX_MD = "/index.md"
Roman Elizarov88396732017-09-27 21:30:47 +0300415val FUNCTIONS_SECTION_HEADER = "### Functions"
416
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300417fun HashMap<String, MutableList<String>>.putUnambiguous(key: String, value: String) {
Roman Elizarov88396732017-09-27 21:30:47 +0300418 val oldValue = this[key]
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300419 if (oldValue != null) {
420 oldValue.add(value)
421 put(key, oldValue)
422 } else {
423 put(key, mutableListOf(value))
424 }
Roman Elizarov88396732017-09-27 21:30:47 +0300425}
Roman Elizarov419a6c82017-02-09 18:36:22 +0300426
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300427fun loadApiIndex(
428 docsRoot: String,
429 path: String,
430 pkg: String,
431 namePrefix: String = ""
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300432): Map<String, MutableList<String>>? {
Roman Elizarove0c817d2017-02-10 10:22:01 +0300433 val fileName = docsRoot + "/" + path + INDEX_MD
Roman Elizarov419a6c82017-02-09 18:36:22 +0300434 val visited = mutableSetOf<String>()
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300435 val map = HashMap<String, MutableList<String>>()
Roman Elizarov88396732017-09-27 21:30:47 +0300436 var inFunctionsSection = false
Roman Elizarov419a6c82017-02-09 18:36:22 +0300437 File(fileName).withLineNumberReader<LineNumberReader>(::LineNumberReader) {
438 while (true) {
439 val line = readLine() ?: break
Roman Elizarov88396732017-09-27 21:30:47 +0300440 if (line == FUNCTIONS_SECTION_HEADER) inFunctionsSection = true
Roman Elizarov419a6c82017-02-09 18:36:22 +0300441 val result = REF_LINE_REGEX.matchEntire(line) ?: continue
Roman Elizarov88396732017-09-27 21:30:47 +0300442 val link = result.groups[1]!!.value
443 if (link.startsWith("..")) continue // ignore cross-references
444 val absLink = path + "/" + link
445 var name = result.groups[2]!!.value
446 // a special disambiguation fix for pseudo-constructor functions
447 if (inFunctionsSection && name[0] in 'A'..'Z') name += "()"
448 val refName = namePrefix + name
449 val fqName = pkg + "." + refName
450 // Put short names for extensions on 3rd party classes (prefix is FQname of those classes)
451 if (namePrefix != "" && namePrefix[0] in 'a'..'z') map.putUnambiguous(name, absLink)
452 // Always put fully qualified names
453 map.putUnambiguous(refName, absLink)
454 map.putUnambiguous(fqName, absLink)
455 if (link.endsWith(INDEX_HTML)) {
456 if (visited.add(link)) {
457 val path2 = path + "/" + link.substring(0, link.length - INDEX_HTML.length)
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300458 map += loadApiIndex(docsRoot, path2, pkg, refName + ".")
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300459 ?: throw IllegalArgumentException("Failed to parse ${docsRoot + "/" + path2}")
Roman Elizarov419a6c82017-02-09 18:36:22 +0300460 }
461 }
462 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300463 } ?: return null // return null on failure
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300464 return map
465}
466
467fun processApiIndex(
468 siteRoot: String,
469 docsRoot: String,
470 pkg: String,
471 remainingApiRefNames: MutableSet<String>
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300472): List<String>? {
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300473 val key = ApiIndexKey(docsRoot, pkg)
Roman Elizarov23f864e2017-03-03 19:57:47 +0300474 val map = apiIndexCache.getOrPut(key, {
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300475 print("Parsing API docs at $docsRoot/$pkg: ")
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300476 val result = loadApiIndex(docsRoot, pkg, pkg) ?: return null // null on failure
Roman Elizarov23f864e2017-03-03 19:57:47 +0300477 println("${result.size} definitions")
478 result
479 })
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300480 val indexList = arrayListOf<String>()
481 val it = remainingApiRefNames.iterator()
482 while (it.hasNext()) {
483 val refName = it.next()
484 val refLink = map[refName] ?: continue
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300485 if (refLink.size > 1) {
486 println("INFO: Ambiguous reference to [$refName]: $refLink, taking the shortest one")
Roman Elizarov88396732017-09-27 21:30:47 +0300487 }
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300488
489 val link = refLink.minBy { it.length }
490 indexList += "[$refName]: $siteRoot/$link"
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300491 it.remove()
492 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300493 return indexList
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300494}