blob: 275fc286dd9a1d500df4f6e201b74977ada65903 [file] [log] [blame]
Roman Elizarovf16fd272017-02-07 11:26:00 +03001/*
2 * Copyright 2016-2017 JetBrains s.r.o.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Roman Elizarove7e2ad12017-05-17 14:47:31 +030017import java.io.File
18import java.io.IOException
19import java.io.LineNumberReader
20import java.io.Reader
21import java.util.*
22import kotlin.properties.Delegates
23
24// --- props in knit.properties
25
26val knitProperties = ClassLoader.getSystemClassLoader()
27 .getResource("knit.properties").openStream().use { Properties().apply { load(it) } }
28
29val siteRoot = knitProperties.getProperty("site.root")!!
30val moduleRoots = knitProperties.getProperty("module.roots").split(" ")
31val moduleMarker = knitProperties.getProperty("module.marker")!!
32val moduleDocs = knitProperties.getProperty("module.docs")!!
33
34// --- markdown syntax
Roman Elizarovb3d55a52017-02-03 12:47:21 +030035
Roman Elizarov731f0ad2017-02-22 20:48:45 +030036const val DIRECTIVE_START = "<!--- "
37const val DIRECTIVE_END = "-->"
Roman Elizarovb3d55a52017-02-03 12:47:21 +030038
Roman Elizarov731f0ad2017-02-22 20:48:45 +030039const val TOC_DIRECTIVE = "TOC"
40const val KNIT_DIRECTIVE = "KNIT"
41const val INCLUDE_DIRECTIVE = "INCLUDE"
42const val CLEAR_DIRECTIVE = "CLEAR"
43const val TEST_DIRECTIVE = "TEST"
Roman Elizarove0c817d2017-02-10 10:22:01 +030044
Roman Elizarov731f0ad2017-02-22 20:48:45 +030045const val TEST_OUT_DIRECTIVE = "TEST_OUT"
Roman Elizarovfa7723e2017-02-06 11:17:51 +030046
Roman Elizarove7e2ad12017-05-17 14:47:31 +030047const val MODULE_DIRECTIVE = "MODULE"
Roman Elizarov731f0ad2017-02-22 20:48:45 +030048const val INDEX_DIRECTIVE = "INDEX"
Roman Elizarovb3d55a52017-02-03 12:47:21 +030049
Roman Elizarov731f0ad2017-02-22 20:48:45 +030050const val CODE_START = "```kotlin"
51const val CODE_END = "```"
52
53const val TEST_START = "```text"
54const val TEST_END = "```"
55
56const val SECTION_START = "##"
57
58const val PACKAGE_PREFIX = "package "
59const val STARTS_WITH_PREDICATE = "STARTS_WITH"
Roman Elizarov1e459602017-02-27 11:05:17 +030060const val ARBITRARY_TIME_PREDICATE = "ARBITRARY_TIME"
Roman Elizarov731f0ad2017-02-22 20:48:45 +030061const val FLEXIBLE_TIME_PREDICATE = "FLEXIBLE_TIME"
62const val FLEXIBLE_THREAD_PREDICATE = "FLEXIBLE_THREAD"
63const val LINES_START_UNORDERED_PREDICATE = "LINES_START_UNORDERED"
64const val LINES_START_PREDICATE = "LINES_START"
Roman Elizarovfa7723e2017-02-06 11:17:51 +030065
Roman Elizarov88396732017-09-27 21:30:47 +030066val API_REF_REGEX = Regex("(^|[ \\]])\\[([A-Za-z0-9_().]+)\\]($|[^\\[\\(])")
Roman Elizarov419a6c82017-02-09 18:36:22 +030067
Roman Elizarovb3d55a52017-02-03 12:47:21 +030068fun main(args: Array<String>) {
Roman Elizarova5e653f2017-02-13 13:49:55 +030069 if (args.isEmpty()) {
70 println("Usage: Knit <markdown-files>")
Roman Elizarovb3d55a52017-02-03 12:47:21 +030071 return
72 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +030073 args.forEach {
74 if (!knit(it)) System.exit(1) // abort on first error with error exit code
75 }
Roman Elizarova5e653f2017-02-13 13:49:55 +030076}
77
Roman Elizarove7e2ad12017-05-17 14:47:31 +030078fun knit(markdownFileName: String): Boolean {
Roman Elizarova5e653f2017-02-13 13:49:55 +030079 println("*** Reading $markdownFileName")
80 val markdownFile = File(markdownFileName)
Roman Elizarov8a4a8e12017-03-09 19:52:58 +030081 val tocLines = arrayListOf<String>()
Roman Elizarovb3d55a52017-02-03 12:47:21 +030082 var knitRegex: Regex? = null
83 val includes = arrayListOf<Include>()
Roman Elizarov8a4a8e12017-03-09 19:52:58 +030084 val codeLines = arrayListOf<String>()
85 val testLines = arrayListOf<String>()
86 var testOut: String? = null
87 val testOutLines = arrayListOf<String>()
Roman Elizarov731f0ad2017-02-22 20:48:45 +030088 var lastPgk: String? = null
Roman Elizarov23f864e2017-03-03 19:57:47 +030089 val files = mutableSetOf<File>()
Roman Elizarov419a6c82017-02-09 18:36:22 +030090 val allApiRefs = arrayListOf<ApiRef>()
91 val remainingApiRefNames = mutableSetOf<String>()
Roman Elizarove7e2ad12017-05-17 14:47:31 +030092 var moduleName: String by Delegates.notNull()
93 var docsRoot: String by Delegates.notNull()
Roman Elizarovfa7723e2017-02-06 11:17:51 +030094 // read markdown file
Roman Elizarov419a6c82017-02-09 18:36:22 +030095 var putBackLine: String? = null
Roman Elizarovfa7723e2017-02-06 11:17:51 +030096 val markdown = markdownFile.withMarkdownTextReader {
Roman Elizarovb3d55a52017-02-03 12:47:21 +030097 mainLoop@ while (true) {
Roman Elizarov419a6c82017-02-09 18:36:22 +030098 val inLine = putBackLine ?: readLine() ?: break
99 putBackLine = null
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300100 val directive = directive(inLine)
101 if (directive != null && markdownPart == MarkdownPart.TOC) {
102 markdownPart = MarkdownPart.POST_TOC
103 postTocText += inLine
104 }
105 when (directive?.name) {
106 TOC_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300107 requireSingleLine(directive)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300108 require(directive.param.isEmpty()) { "$TOC_DIRECTIVE directive must not have parameters" }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300109 require(markdownPart == MarkdownPart.PRE_TOC) { "Only one TOC directive is supported" }
110 markdownPart = MarkdownPart.TOC
111 }
112 KNIT_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300113 requireSingleLine(directive)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300114 require(!directive.param.isEmpty()) { "$KNIT_DIRECTIVE directive must include regex parameter" }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300115 require(knitRegex == null) { "Only one KNIT directive is supported"}
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300116 knitRegex = Regex("\\((" + directive.param + ")\\)")
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300117 continue@mainLoop
118 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300119 INCLUDE_DIRECTIVE -> {
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300120 if (directive.param.isEmpty()) {
121 require(!directive.singleLine) { "$INCLUDE_DIRECTIVE directive without parameters must not be single line" }
122 readUntilTo(DIRECTIVE_END, codeLines)
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300123 } else {
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300124 val include = Include(Regex(directive.param))
125 if (directive.singleLine) {
126 include.lines += codeLines
127 codeLines.clear()
128 } else {
129 readUntilTo(DIRECTIVE_END, include.lines)
130 }
131 includes += include
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300132 }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300133 continue@mainLoop
134 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300135 CLEAR_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300136 requireSingleLine(directive)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300137 require(directive.param.isEmpty()) { "$CLEAR_DIRECTIVE directive must not have parameters" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300138 codeLines.clear()
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300139 continue@mainLoop
140 }
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300141 TEST_OUT_DIRECTIVE -> {
142 require(!directive.param.isEmpty()) { "$TEST_OUT_DIRECTIVE directive must include file name parameter" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300143 flushTestOut(markdownFile.parentFile, testOut, testOutLines)
144 testOut = directive.param
145 readUntil(DIRECTIVE_END).forEach { testOutLines += it }
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300146 }
147 TEST_DIRECTIVE -> {
148 require(lastPgk != null) { "'$PACKAGE_PREFIX' prefix was not found in emitted code"}
149 require(testOut != null) { "$TEST_OUT_DIRECTIVE directive was not specified" }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300150 val predicate = directive.param
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300151 if (testLines.isEmpty()) {
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300152 if (directive.singleLine) {
153 require(!predicate.isEmpty()) { "$TEST_OUT_DIRECTIVE must be preceded by $TEST_START block or contain test predicate"}
154 } else
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300155 testLines += readUntil(DIRECTIVE_END)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300156 } else {
157 requireSingleLine(directive)
158 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300159 makeTest(testOutLines, lastPgk!!, testLines, predicate)
160 testLines.clear()
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300161 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300162 MODULE_DIRECTIVE -> {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300163 requireSingleLine(directive)
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300164 moduleName = directive.param
165 docsRoot = findModuleRootDir(moduleName) + "/" + moduleDocs + "/" + moduleName
Roman Elizarove0c817d2017-02-10 10:22:01 +0300166 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300167 INDEX_DIRECTIVE -> {
168 requireSingleLine(directive)
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300169 val indexLines = processApiIndex(siteRoot + "/" + moduleName, docsRoot, directive.param, remainingApiRefNames)
170 ?: throw IllegalArgumentException("Failed to load index for ${directive.param}")
Roman Elizarov419a6c82017-02-09 18:36:22 +0300171 skip = true
172 while (true) {
173 val skipLine = readLine() ?: break@mainLoop
174 if (directive(skipLine) != null) {
175 putBackLine = skipLine
176 break
177 }
178 }
179 skip = false
Roman Elizarova5e653f2017-02-13 13:49:55 +0300180 outText += indexLines
181 outText += putBackLine!!
Roman Elizarov419a6c82017-02-09 18:36:22 +0300182 }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300183 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300184 if (inLine.startsWith(CODE_START)) {
Roman Elizarovf724f6e2017-04-07 18:06:22 +0300185 require(testOut == null || testLines.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300186 codeLines += ""
187 readUntilTo(CODE_END, codeLines)
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300188 continue@mainLoop
189 }
190 if (inLine.startsWith(TEST_START)) {
Roman Elizarovf724f6e2017-04-07 18:06:22 +0300191 require(testOut == null || testLines.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300192 readUntilTo(TEST_END, testLines)
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300193 continue@mainLoop
194 }
195 if (inLine.startsWith(SECTION_START) && markdownPart == MarkdownPart.POST_TOC) {
196 val i = inLine.indexOf(' ')
197 require(i >= 2) { "Invalid section start" }
198 val name = inLine.substring(i + 1).trim()
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300199 tocLines += " ".repeat(i - 2) + "* [$name](#${makeSectionRef(name)})"
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300200 continue@mainLoop
201 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300202 for (match in API_REF_REGEX.findAll(inLine)) {
203 val apiRef = ApiRef(lineNumber, match.groups[2]!!.value)
204 allApiRefs += apiRef
205 remainingApiRefNames += apiRef.name
206 }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300207 knitRegex?.find(inLine)?.let { knitMatch ->
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300208 val fileName = knitMatch.groups[1]!!.value
Roman Elizarov23f864e2017-03-03 19:57:47 +0300209 val file = File(markdownFile.parentFile, fileName)
210 require(files.add(file)) { "Duplicate file: $file"}
211 println("Knitting $file ...")
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300212 val outLines = arrayListOf<String>()
213 for (include in includes) {
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300214 val includeMatch = include.regex.matchEntire(fileName) ?: continue
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300215 include.lines.forEach { includeLine ->
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300216 val line = makeReplacements(includeLine, includeMatch)
217 if (line.startsWith(PACKAGE_PREFIX))
218 lastPgk = line.substring(PACKAGE_PREFIX.length).trim()
219 outLines += line
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300220 }
221 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300222 outLines += codeLines
223 codeLines.clear()
224 writeLinesIfNeeded(file, outLines)
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300225 }
226 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300227 } ?: return false // false when failed
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300228 // update markdown file with toc
Roman Elizarova5e653f2017-02-13 13:49:55 +0300229 val newLines = buildList<String> {
230 addAll(markdown.preTocText)
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300231 if (!tocLines.isEmpty()) {
Roman Elizarova5e653f2017-02-13 13:49:55 +0300232 add("")
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300233 addAll(tocLines)
Roman Elizarova5e653f2017-02-13 13:49:55 +0300234 add("")
235 }
236 addAll(markdown.postTocText)
237 }
238 if (newLines != markdown.inText) writeLines(markdownFile, newLines)
Roman Elizarov419a6c82017-02-09 18:36:22 +0300239 // check apiRefs
240 for (apiRef in allApiRefs) {
241 if (apiRef.name in remainingApiRefNames) {
242 println("WARNING: $markdownFile: ${apiRef.line}: Broken reference to [${apiRef.name}]")
243 }
244 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300245 // write test output
246 flushTestOut(markdownFile.parentFile, testOut, testOutLines)
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300247 return true
Roman Elizarov419a6c82017-02-09 18:36:22 +0300248}
249
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300250fun makeTest(testOutLines: MutableList<String>, pgk: String, test: List<String>, predicate: String) {
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300251 val funName = buildString {
252 var cap = true
253 for (c in pgk) {
254 if (c == '.') {
255 cap = true
256 } else {
257 append(if (cap) c.toUpperCase() else c)
258 cap = false
259 }
260 }
261 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300262 testOutLines += ""
263 testOutLines += " @Test"
264 testOutLines += " fun test$funName() {"
Roman Elizarovba0c0042017-07-12 13:01:17 +0300265 val prefix = " test(\"$funName\") { $pgk.main(emptyArray()) }"
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300266 when (predicate) {
267 "" -> makeTestLines(testOutLines, prefix, "verifyLines", test)
268 STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStartWith", test)
269 ARBITRARY_TIME_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesArbitraryTime", test)
270 FLEXIBLE_TIME_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesFlexibleTime", test)
271 FLEXIBLE_THREAD_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesFlexibleThread", test)
272 LINES_START_UNORDERED_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStartUnordered", test)
273 LINES_START_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStart", test)
274 else -> {
275 testOutLines += prefix + ".also { lines ->"
276 testOutLines += " check($predicate)"
277 testOutLines += " }"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300278 }
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300279 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300280 testOutLines += " }"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300281}
282
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300283private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, method: String, test: List<String>) {
284 testOutLines += "$prefix.$method("
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300285 for ((index, testLine) in test.withIndex()) {
286 val commaOpt = if (index < test.size - 1) "," else ""
287 val escapedLine = testLine.replace("\"", "\\\"")
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300288 testOutLines += " \"$escapedLine\"$commaOpt"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300289 }
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300290 testOutLines += " )"
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300291}
292
293private fun makeReplacements(line: String, match: MatchResult): String {
294 var result = line
295 for ((id, group) in match.groups.withIndex()) {
296 if (group != null)
297 result = result.replace("\$\$$id", group.value)
298 }
299 return result
300}
301
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300302private fun flushTestOut(parentDir: File?, testOut: String?, testOutLines: MutableList<String>) {
303 if (testOut == null) return
304 val file = File(parentDir, testOut)
305 testOutLines += "}"
306 writeLinesIfNeeded(file, testOutLines)
307 testOutLines.clear()
Roman Elizarov731f0ad2017-02-22 20:48:45 +0300308}
309
310private fun MarkdownTextReader.readUntil(marker: String): List<String> =
311 arrayListOf<String>().also { readUntilTo(marker, it) }
312
313private fun MarkdownTextReader.readUntilTo(marker: String, list: MutableList<String>) {
314 while (true) {
315 val line = readLine() ?: break
316 if (line.startsWith(marker)) break
317 list += line
318 }
319}
320
Roman Elizarova5e653f2017-02-13 13:49:55 +0300321private inline fun <T> buildList(block: ArrayList<T>.() -> Unit): List<T> {
322 val result = arrayListOf<T>()
323 result.block()
324 return result
325}
326
Roman Elizarov419a6c82017-02-09 18:36:22 +0300327private fun requireSingleLine(directive: Directive) {
328 require(directive.singleLine) { "${directive.name} directive must end on the same line with '$DIRECTIVE_END'" }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300329}
330
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300331fun makeSectionRef(name: String): String = name.replace(' ', '-').replace(".", "").toLowerCase()
332
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300333class Include(val regex: Regex, val lines: MutableList<String> = arrayListOf())
334
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300335class Directive(
336 val name: String,
337 val param: String,
338 val singleLine: Boolean
339)
340
341fun directive(line: String): Directive? {
342 if (!line.startsWith(DIRECTIVE_START)) return null
343 var s = line.substring(DIRECTIVE_START.length).trim()
344 val singleLine = s.endsWith(DIRECTIVE_END)
345 if (singleLine) s = s.substring(0, s.length - DIRECTIVE_END.length)
346 val i = s.indexOf(' ')
347 val name = if (i < 0) s else s.substring(0, i)
348 val param = if (i < 0) "" else s.substring(i).trim()
349 return Directive(name, param, singleLine)
350}
351
Roman Elizarov419a6c82017-02-09 18:36:22 +0300352class ApiRef(val line: Int, val name: String)
353
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300354enum class MarkdownPart { PRE_TOC, TOC, POST_TOC }
355
356class MarkdownTextReader(r: Reader) : LineNumberReader(r) {
Roman Elizarova5e653f2017-02-13 13:49:55 +0300357 val inText = arrayListOf<String>()
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300358 val preTocText = arrayListOf<String>()
359 val postTocText = arrayListOf<String>()
360 var markdownPart: MarkdownPart = MarkdownPart.PRE_TOC
Roman Elizarov419a6c82017-02-09 18:36:22 +0300361 var skip = false
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300362
Roman Elizarova5e653f2017-02-13 13:49:55 +0300363 val outText: MutableList<String> get() = when (markdownPart) {
364 MarkdownPart.PRE_TOC -> preTocText
365 MarkdownPart.POST_TOC -> postTocText
366 else -> throw IllegalStateException("Wrong state: $markdownPart")
367 }
368
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300369 override fun readLine(): String? {
370 val line = super.readLine() ?: return null
Roman Elizarova5e653f2017-02-13 13:49:55 +0300371 inText += line
372 if (!skip && markdownPart != MarkdownPart.TOC)
373 outText += line
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300374 return line
375 }
376}
377
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300378fun <T : LineNumberReader> File.withLineNumberReader(factory: (Reader) -> T, block: T.() -> Unit): T? {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300379 val reader = factory(reader())
380 reader.use {
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300381 try {
382 it.block()
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300383 } catch (e: Exception) {
Roman Elizarov419a6c82017-02-09 18:36:22 +0300384 println("ERROR: ${this@withLineNumberReader}: ${it.lineNumber}: ${e.message}")
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300385 return null
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300386 }
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300387 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300388 return reader
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300389}
390
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300391fun File.withMarkdownTextReader(block: MarkdownTextReader.() -> Unit): MarkdownTextReader? =
Roman Elizarov419a6c82017-02-09 18:36:22 +0300392 withLineNumberReader<MarkdownTextReader>(::MarkdownTextReader, block)
393
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300394fun writeLinesIfNeeded(file: File, outLines: List<String>) {
395 val oldLines = try {
396 file.readLines()
397 } catch (e: IOException) {
398 emptyList<String>()
399 }
400 if (outLines != oldLines) writeLines(file, outLines)
401}
402
Roman Elizarov419a6c82017-02-09 18:36:22 +0300403fun writeLines(file: File, lines: List<String>) {
Roman Elizarovfa7723e2017-02-06 11:17:51 +0300404 println(" Writing $file ...")
405 file.parentFile?.mkdirs()
406 file.printWriter().use { out ->
407 lines.forEach { out.println(it) }
Roman Elizarovb3d55a52017-02-03 12:47:21 +0300408 }
409}
Roman Elizarov419a6c82017-02-09 18:36:22 +0300410
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300411fun findModuleRootDir(name: String): String =
412 moduleRoots
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300413 .map { "$it/$name" }
414 .firstOrNull { File("$it/$moduleMarker").exists() }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300415 ?: throw IllegalArgumentException("Module $name is not found in any of the module root dirs")
416
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300417data class ApiIndexKey(
418 val docsRoot: String,
419 val pkg: String
420)
421
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300422val apiIndexCache: MutableMap<ApiIndexKey, Map<String, List<String>>> = HashMap()
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300423
Roman Elizarov419a6c82017-02-09 18:36:22 +0300424val REF_LINE_REGEX = Regex("<a href=\"([a-z/.\\-]+)\">([a-zA-z.]+)</a>")
425val INDEX_HTML = "/index.html"
426val INDEX_MD = "/index.md"
Roman Elizarov88396732017-09-27 21:30:47 +0300427val FUNCTIONS_SECTION_HEADER = "### Functions"
428
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300429fun HashMap<String, MutableList<String>>.putUnambiguous(key: String, value: String) {
Roman Elizarov88396732017-09-27 21:30:47 +0300430 val oldValue = this[key]
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300431 if (oldValue != null) {
432 oldValue.add(value)
433 put(key, oldValue)
434 } else {
435 put(key, mutableListOf(value))
436 }
Roman Elizarov88396732017-09-27 21:30:47 +0300437}
Roman Elizarov419a6c82017-02-09 18:36:22 +0300438
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300439fun loadApiIndex(
440 docsRoot: String,
441 path: String,
442 pkg: String,
443 namePrefix: String = ""
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300444): Map<String, MutableList<String>>? {
Roman Elizarove0c817d2017-02-10 10:22:01 +0300445 val fileName = docsRoot + "/" + path + INDEX_MD
Roman Elizarov419a6c82017-02-09 18:36:22 +0300446 val visited = mutableSetOf<String>()
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300447 val map = HashMap<String, MutableList<String>>()
Roman Elizarov88396732017-09-27 21:30:47 +0300448 var inFunctionsSection = false
Roman Elizarov419a6c82017-02-09 18:36:22 +0300449 File(fileName).withLineNumberReader<LineNumberReader>(::LineNumberReader) {
450 while (true) {
451 val line = readLine() ?: break
Roman Elizarov88396732017-09-27 21:30:47 +0300452 if (line == FUNCTIONS_SECTION_HEADER) inFunctionsSection = true
Roman Elizarov419a6c82017-02-09 18:36:22 +0300453 val result = REF_LINE_REGEX.matchEntire(line) ?: continue
Roman Elizarov88396732017-09-27 21:30:47 +0300454 val link = result.groups[1]!!.value
455 if (link.startsWith("..")) continue // ignore cross-references
456 val absLink = path + "/" + link
457 var name = result.groups[2]!!.value
458 // a special disambiguation fix for pseudo-constructor functions
459 if (inFunctionsSection && name[0] in 'A'..'Z') name += "()"
460 val refName = namePrefix + name
461 val fqName = pkg + "." + refName
462 // Put short names for extensions on 3rd party classes (prefix is FQname of those classes)
463 if (namePrefix != "" && namePrefix[0] in 'a'..'z') map.putUnambiguous(name, absLink)
464 // Always put fully qualified names
465 map.putUnambiguous(refName, absLink)
466 map.putUnambiguous(fqName, absLink)
467 if (link.endsWith(INDEX_HTML)) {
468 if (visited.add(link)) {
469 val path2 = path + "/" + link.substring(0, link.length - INDEX_HTML.length)
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300470 map += loadApiIndex(docsRoot, path2, pkg, refName + ".")
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300471 ?: throw IllegalArgumentException("Failed to parse ${docsRoot + "/" + path2}")
Roman Elizarov419a6c82017-02-09 18:36:22 +0300472 }
473 }
474 }
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300475 } ?: return null // return null on failure
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300476 return map
477}
478
479fun processApiIndex(
480 siteRoot: String,
481 docsRoot: String,
482 pkg: String,
483 remainingApiRefNames: MutableSet<String>
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300484): List<String>? {
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300485 val key = ApiIndexKey(docsRoot, pkg)
Roman Elizarov23f864e2017-03-03 19:57:47 +0300486 val map = apiIndexCache.getOrPut(key, {
Roman Elizarov8a4a8e12017-03-09 19:52:58 +0300487 print("Parsing API docs at $docsRoot/$pkg: ")
Roman Elizarove7e2ad12017-05-17 14:47:31 +0300488 val result = loadApiIndex(docsRoot, pkg, pkg) ?: return null // null on failure
Roman Elizarov23f864e2017-03-03 19:57:47 +0300489 println("${result.size} definitions")
490 result
491 })
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300492 val indexList = arrayListOf<String>()
493 val it = remainingApiRefNames.iterator()
494 while (it.hasNext()) {
495 val refName = it.next()
496 val refLink = map[refName] ?: continue
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300497 if (refLink.size > 1) {
498 println("INFO: Ambiguous reference to [$refName]: $refLink, taking the shortest one")
Roman Elizarov88396732017-09-27 21:30:47 +0300499 }
Vsevolod Tolstopyatov9c692792018-04-11 18:22:35 +0300500
501 val link = refLink.minBy { it.length }
502 indexList += "[$refName]: $siteRoot/$link"
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300503 it.remove()
504 }
Roman Elizarov419a6c82017-02-09 18:36:22 +0300505 return indexList
Roman Elizarovd4dcbe22017-02-22 09:57:46 +0300506}