• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 
5 import java.io.*
6 import java.util.*
7 import kotlin.properties.*
8 
9 // --- props in knit.properties
10 
11 val knitProperties = ClassLoader.getSystemClassLoader()
12     .getResource("knit.properties").openStream().use { Properties().apply { load(it) } }
13 
14 val siteRoot = knitProperties.getProperty("site.root")!!
15 val moduleRoots = knitProperties.getProperty("module.roots").split(" ")
16 val moduleMarker = knitProperties.getProperty("module.marker")!!
17 val moduleDocs = knitProperties.getProperty("module.docs")!!
18 
19 // --- markdown syntax
20 
21 const val DIRECTIVE_START = "<!--- "
22 const val DIRECTIVE_END = "-->"
23 
24 const val TOC_DIRECTIVE = "TOC"
25 const val TOC_REF_DIRECTIVE = "TOC_REF"
26 const val KNIT_DIRECTIVE = "KNIT"
27 const val INCLUDE_DIRECTIVE = "INCLUDE"
28 const val CLEAR_DIRECTIVE = "CLEAR"
29 const val TEST_DIRECTIVE = "TEST"
30 
31 const val KNIT_AUTONUMBER_PLACEHOLDER = '#'
32 const val KNIT_AUTONUMBER_REGEX = "([0-9a-z]+)"
33 
34 const val TEST_OUT_DIRECTIVE = "TEST_OUT"
35 
36 const val MODULE_DIRECTIVE = "MODULE"
37 const val INDEX_DIRECTIVE = "INDEX"
38 
39 const val CODE_START = "```kotlin"
40 const val CODE_END = "```"
41 
42 const val SAMPLE_START = "//sampleStart"
43 const val SAMPLE_END = "//sampleEnd"
44 
45 const val TEST_START = "```text"
46 const val TEST_END = "```"
47 
48 const val SECTION_START = "##"
49 
50 const val PACKAGE_PREFIX = "package "
51 const val STARTS_WITH_PREDICATE = "STARTS_WITH"
52 const val ARBITRARY_TIME_PREDICATE = "ARBITRARY_TIME"
53 const val FLEXIBLE_TIME_PREDICATE = "FLEXIBLE_TIME"
54 const val FLEXIBLE_THREAD_PREDICATE = "FLEXIBLE_THREAD"
55 const val LINES_START_UNORDERED_PREDICATE = "LINES_START_UNORDERED"
56 const val EXCEPTION_MODE = "EXCEPTION"
57 const val LINES_START_PREDICATE = "LINES_START"
58 
59 val API_REF_REGEX = Regex("(^|[ \\](])\\[([A-Za-z0-9_().]+)]($|[^\\[(])")
60 val LINK_DEF_REGEX = Regex("^\\[([A-Za-z0-9_().]+)]: .*")
61 
62 val tocRefMap = HashMap<File, List<TocRef>>()
63 val fileSet = HashSet<File>()
64 val fileQueue = ArrayDeque<File>()
65 
mainnull66 fun main(args: Array<String>) {
67     if (args.isEmpty()) {
68         println("Usage: Knit <markdown-files>")
69         return
70     }
71     args.map { File(it) }.toCollection(fileQueue)
72     fileQueue.toCollection(fileSet)
73     while (!fileQueue.isEmpty()) {
74         if (!knit(fileQueue.removeFirst())) System.exit(1) // abort on first error with error exit code
75     }
76 }
77 
knitnull78 fun knit(markdownFile: File): Boolean {
79     println("*** Reading $markdownFile")
80     val tocLines = arrayListOf<String>()
81     var knitRegex: Regex? = null
82     var knitAutonumberGroup = 0
83     var knitAutonumberDigits = 0
84     var knitAutonumberIndex = 1
85     val includes = arrayListOf<Include>()
86     val codeLines = arrayListOf<String>()
87     val testLines = arrayListOf<String>()
88     var testOut: String? = null
89     val testOutLines = arrayListOf<String>()
90     var lastPgk: String? = null
91     val files = mutableSetOf<File>()
92     val allApiRefs = arrayListOf<ApiRef>()
93     val remainingApiRefNames = mutableSetOf<String>()
94     var moduleName: String by Delegates.notNull()
95     var docsRoot: String by Delegates.notNull()
96     var retryKnitLater = false
97     val tocRefs = ArrayList<TocRef>().also { tocRefMap[markdownFile] = it }
98     // read markdown file
99     val markdown = markdownFile.withMarkdownTextReader {
100         mainLoop@ while (true) {
101             val inLine = readLine() ?: break
102             val directive = directive(inLine)
103             if (directive != null && markdownPart == MarkdownPart.TOC) {
104                 markdownPart = MarkdownPart.POST_TOC
105                 postTocText += inLine
106             }
107             when (directive?.name) {
108                 TOC_DIRECTIVE -> {
109                     requireSingleLine(directive)
110                     require(directive.param.isEmpty()) { "$TOC_DIRECTIVE directive must not have parameters" }
111                     require(markdownPart == MarkdownPart.PRE_TOC) { "Only one TOC directive is supported" }
112                     markdownPart = MarkdownPart.TOC
113                 }
114                 TOC_REF_DIRECTIVE -> {
115                     requireSingleLine(directive)
116                     require(!directive.param.isEmpty()) { "$TOC_REF_DIRECTIVE directive must include reference file path" }
117                     val refPath = directive.param
118                     val refFile = File(markdownFile.parent, refPath.replace('/', File.separatorChar))
119                     require(fileSet.contains(refFile)) { "Referenced file $refFile is missing from the processed file set" }
120                     val toc = tocRefMap[refFile]
121                     if (toc == null) {
122                         retryKnitLater = true // put this file at the end of the queue and retry later
123                     } else {
124                         val lines = toc.map { (levelPrefix, name, ref) ->
125                             "$levelPrefix <a name='$ref'></a>[$name]($refPath#$ref)"
126                         }
127                         if (!replaceUntilNextDirective(lines)) error("Unexpected end of file after $TOC_REF_DIRECTIVE")
128                     }
129                 }
130                 KNIT_DIRECTIVE -> {
131                     requireSingleLine(directive)
132                     require(!directive.param.isEmpty()) { "$KNIT_DIRECTIVE directive must include regex parameter" }
133                     require(knitRegex == null) { "Only one KNIT directive is supported"}
134                     var str = directive.param
135                     val i = str.indexOf(KNIT_AUTONUMBER_PLACEHOLDER)
136                     if (i >= 0) {
137                         val j = str.lastIndexOf(KNIT_AUTONUMBER_PLACEHOLDER)
138                         knitAutonumberDigits = j - i + 1
139                         require(str.substring(i, j + 1) == KNIT_AUTONUMBER_PLACEHOLDER.toString().repeat(knitAutonumberDigits)) {
140                             "$KNIT_DIRECTIVE can only use a contiguous range of '$KNIT_AUTONUMBER_PLACEHOLDER' for auto-numbering"
141                         }
142                         knitAutonumberGroup = str.substring(0, i).count { it == '(' } + 2 // note: it does not understand escaped open braces
143                         str = str.substring(0, i) + KNIT_AUTONUMBER_REGEX + str.substring(j + 1)
144                     }
145                     knitRegex = Regex("\\((" + str + ")\\)")
146                     continue@mainLoop
147                 }
148                 INCLUDE_DIRECTIVE -> {
149                     if (directive.param.isEmpty()) {
150                         require(!directive.singleLine) { "$INCLUDE_DIRECTIVE directive without parameters must not be single line" }
151                         readUntilTo(DIRECTIVE_END, codeLines)
152                     } else {
153                         val include = Include(Regex(directive.param))
154                         if (directive.singleLine) {
155                             include.lines += codeLines
156                             codeLines.clear()
157                         } else {
158                             readUntilTo(DIRECTIVE_END, include.lines)
159                         }
160                         includes += include
161                     }
162                     continue@mainLoop
163                 }
164                 CLEAR_DIRECTIVE -> {
165                     requireSingleLine(directive)
166                     require(directive.param.isEmpty()) { "$CLEAR_DIRECTIVE directive must not have parameters" }
167                     codeLines.clear()
168                     continue@mainLoop
169                 }
170                 TEST_OUT_DIRECTIVE -> {
171                     require(!directive.param.isEmpty()) { "$TEST_OUT_DIRECTIVE directive must include file name parameter" }
172                     flushTestOut(markdownFile.parentFile, testOut, testOutLines)
173                     testOut = directive.param
174                     readUntil(DIRECTIVE_END).forEach { testOutLines += it }
175                 }
176                 TEST_DIRECTIVE -> {
177                     require(lastPgk != null) { "'$PACKAGE_PREFIX' prefix was not found in emitted code"}
178                     require(testOut != null) { "$TEST_OUT_DIRECTIVE directive was not specified" }
179                     val predicate = directive.param
180                     if (testLines.isEmpty()) {
181                         if (directive.singleLine) {
182                             require(!predicate.isEmpty()) { "$TEST_OUT_DIRECTIVE must be preceded by $TEST_START block or contain test predicate"}
183                         } else
184                             testLines += readUntil(DIRECTIVE_END)
185                     } else {
186                         requireSingleLine(directive)
187                     }
188                     makeTest(testOutLines, lastPgk!!, testLines, predicate)
189                     testLines.clear()
190                 }
191                 MODULE_DIRECTIVE -> {
192                     requireSingleLine(directive)
193                     moduleName = directive.param
194                     docsRoot = findModuleRootDir(moduleName) + "/" + moduleDocs + "/" + moduleName
195                 }
196                 INDEX_DIRECTIVE -> {
197                     requireSingleLine(directive)
198                     val indexLines = processApiIndex("$siteRoot/$moduleName", docsRoot, directive.param, remainingApiRefNames)
199                         ?: throw IllegalArgumentException("Failed to load index for ${directive.param}")
200                     if (!replaceUntilNextDirective(indexLines)) error("Unexpected end of file after $INDEX_DIRECTIVE")
201                 }
202             }
203             if (inLine.startsWith(CODE_START)) {
204                 require(testOut == null || testLines.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
205                 codeLines += ""
206                 readUntilTo(CODE_END, codeLines) { line ->
207                     !line.startsWith(SAMPLE_START) && !line.startsWith(SAMPLE_END)
208                 }
209                 continue@mainLoop
210             }
211             if (inLine.startsWith(TEST_START)) {
212                 require(testOut == null || testLines.isEmpty()) { "Previous test was not emitted with $TEST_DIRECTIVE" }
213                 readUntilTo(TEST_END, testLines)
214                 continue@mainLoop
215             }
216             if (inLine.startsWith(SECTION_START) && markdownPart == MarkdownPart.POST_TOC) {
217                 val i = inLine.indexOf(' ')
218                 require(i >= 2) { "Invalid section start" }
219                 val name = inLine.substring(i + 1).trim()
220                 val levelPrefix = "  ".repeat(i - 2) + "*"
221                 val sectionRef = makeSectionRef(name)
222                 tocLines += "$levelPrefix [$name](#$sectionRef)"
223                 tocRefs += TocRef(levelPrefix, name, sectionRef)
224                 continue@mainLoop
225             }
226             val linkDefMatch = LINK_DEF_REGEX.matchEntire(inLine)
227             if (linkDefMatch != null) {
228                 val name = linkDefMatch.groups[1]!!.value
229                 remainingApiRefNames -= name
230             } else {
231                 for (match in API_REF_REGEX.findAll(inLine)) {
232                     val apiRef = ApiRef(lineNumber, match.groups[2]!!.value)
233                     allApiRefs += apiRef
234                     remainingApiRefNames += apiRef.name
235                 }
236             }
237             knitRegex?.find(inLine)?.let knitRegexMatch@{ knitMatch ->
238                 val fileName = knitMatch.groups[1]!!.value
239                 if (knitAutonumberDigits != 0) {
240                     val numGroup = knitMatch.groups[knitAutonumberGroup]!!
241                     val num = knitAutonumberIndex.toString().padStart(knitAutonumberDigits, '0')
242                     if (numGroup.value != num) { // update and retry with this line if a different number
243                         val r = numGroup.range
244                         val newLine = inLine.substring(0, r.first) + num + inLine.substring(r.last + 1)
245                         updateLineAndRetry(newLine)
246                         return@knitRegexMatch
247                     }
248                 }
249                 knitAutonumberIndex++
250                 val file = File(markdownFile.parentFile, fileName)
251                 require(files.add(file)) { "Duplicate file: $file"}
252                 println("Knitting $file ...")
253                 val outLines = arrayListOf<String>()
254                 for (include in includes) {
255                     val includeMatch = include.regex.matchEntire(fileName) ?: continue
256                     include.lines.forEach { includeLine ->
257                         val line = makeReplacements(includeLine, includeMatch)
258                         if (line.startsWith(PACKAGE_PREFIX))
259                             lastPgk = line.substring(PACKAGE_PREFIX.length).trim()
260                         outLines += line
261                     }
262                 }
263                 for (code in codeLines) {
264                     outLines += code.replace("System.currentTimeMillis()", "currentTimeMillis()")
265                 }
266                 codeLines.clear()
267                 writeLinesIfNeeded(file, outLines)
268             }
269         }
270     } ?: return false // false when failed
271     // bailout if retry was requested
272     if (retryKnitLater) {
273         fileQueue.add(markdownFile)
274         return true
275     }
276     // update markdown file with toc
277     val newLines = buildList<String> {
278         addAll(markdown.preTocText)
279         if (!tocLines.isEmpty()) {
280             add("")
281             addAll(tocLines)
282             add("")
283         }
284         addAll(markdown.postTocText)
285     }
286     if (newLines != markdown.inText) writeLines(markdownFile, newLines)
287     // check apiRefs
288     for (apiRef in allApiRefs) {
289         if (apiRef.name in remainingApiRefNames) {
290             println("WARNING: $markdownFile: ${apiRef.line}: Broken reference to [${apiRef.name}]")
291         }
292     }
293     // write test output
294     flushTestOut(markdownFile.parentFile, testOut, testOutLines)
295     return true
296 }
297 
298 data class TocRef(val levelPrefix: String, val name: String, val ref: String)
299 
makeTestnull300 fun makeTest(testOutLines: MutableList<String>, pgk: String, test: List<String>, predicate: String) {
301     val funName = buildString {
302         var cap = true
303         for (c in pgk) {
304             if (c == '.') {
305                 cap = true
306             } else {
307                 append(if (cap) c.toUpperCase() else c)
308                 cap = false
309             }
310         }
311     }
312     testOutLines += ""
313     testOutLines += "    @Test"
314     testOutLines += "    fun test$funName() {"
315     val prefix = "        test(\"$funName\") { $pgk.main() }"
316     when (predicate) {
317         "" -> makeTestLines(testOutLines, prefix, "verifyLines", test)
318         STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStartWith", test)
319         ARBITRARY_TIME_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesArbitraryTime", test)
320         FLEXIBLE_TIME_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesFlexibleTime", test)
321         FLEXIBLE_THREAD_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesFlexibleThread", test)
322         LINES_START_UNORDERED_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStartUnordered", test)
323         EXCEPTION_MODE -> makeTestLines(testOutLines, prefix, "verifyExceptions", test)
324         LINES_START_PREDICATE -> makeTestLines(testOutLines, prefix, "verifyLinesStart", test)
325         else -> {
326             testOutLines += "$prefix.also { lines ->"
327             testOutLines += "            check($predicate)"
328             testOutLines += "        }"
329         }
330     }
331     testOutLines += "    }"
332 }
333 
makeTestLinesnull334 private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, method: String, test: List<String>) {
335     testOutLines += "$prefix.$method("
336     for ((index, testLine) in test.withIndex()) {
337         val commaOpt = if (index < test.size - 1) "," else ""
338         val escapedLine = testLine.replace("\"", "\\\"")
339         testOutLines += "            \"$escapedLine\"$commaOpt"
340     }
341     testOutLines += "        )"
342 }
343 
makeReplacementsnull344 private fun makeReplacements(line: String, match: MatchResult): String {
345     var result = line
346     for ((id, group) in match.groups.withIndex()) {
347         if (group != null)
348             result = result.replace("\$\$$id", group.value)
349     }
350     return result
351 }
352 
flushTestOutnull353 private fun flushTestOut(parentDir: File?, testOut: String?, testOutLines: MutableList<String>) {
354     if (testOut == null) return
355     val file = File(parentDir, testOut)
356     testOutLines += "}"
357     writeLinesIfNeeded(file, testOutLines)
358     testOutLines.clear()
359 }
360 
MarkdownTextReadernull361 private fun MarkdownTextReader.readUntil(marker: String): List<String> =
362     arrayListOf<String>().also { readUntilTo(marker, it) }
363 
<lambda>null364 private fun MarkdownTextReader.readUntilTo(marker: String, list: MutableList<String>, linePredicate: (String) -> Boolean = { true }) {
365     while (true) {
366         val line = readLine() ?: break
367         if (line.startsWith(marker)) break
368         if (linePredicate(line)) list += line
369     }
370 }
371 
buildListnull372 private inline fun <T> buildList(block: ArrayList<T>.() -> Unit): List<T> {
373     val result = arrayListOf<T>()
374     result.block()
375     return result
376 }
377 
requireSingleLinenull378 private fun requireSingleLine(directive: Directive) {
379     require(directive.singleLine) { "${directive.name} directive must end on the same line with '$DIRECTIVE_END'" }
380 }
381 
makeSectionRefnull382 fun makeSectionRef(name: String): String = name
383     .replace(' ', '-')
384     .replace(".", "")
385     .replace(",", "")
386     .replace("(", "")
387     .replace(")", "")
388     .replace("`", "")
389     .toLowerCase()
390 
391 class Include(val regex: Regex, val lines: MutableList<String> = arrayListOf())
392 
393 class Directive(
394     val name: String,
395     val param: String,
396     val singleLine: Boolean
397 )
398 
399 fun directive(line: String): Directive? {
400     if (!line.startsWith(DIRECTIVE_START)) return null
401     var s = line.substring(DIRECTIVE_START.length).trim()
402     val singleLine = s.endsWith(DIRECTIVE_END)
403     if (singleLine) s = s.substring(0, s.length - DIRECTIVE_END.length)
404     val i = s.indexOf(' ')
405     val name = if (i < 0) s else s.substring(0, i)
406     val param = if (i < 0) "" else s.substring(i).trim()
407     return Directive(name, param, singleLine)
408 }
409 
410 class ApiRef(val line: Int, val name: String)
411 
412 enum class MarkdownPart { PRE_TOC, TOC, POST_TOC }
413 
414 class MarkdownTextReader(r: Reader) : LineNumberReader(r) {
415     val inText = arrayListOf<String>()
416     val preTocText = arrayListOf<String>()
417     val postTocText = arrayListOf<String>()
418     var markdownPart: MarkdownPart = MarkdownPart.PRE_TOC
419     var skip = false
420     var putBackLine: String? = null
421 
422     val outText: MutableList<String> get() = when (markdownPart) {
423         MarkdownPart.PRE_TOC -> preTocText
424         MarkdownPart.POST_TOC -> postTocText
425         else -> throw IllegalStateException("Wrong state: $markdownPart")
426     }
427 
readLinenull428     override fun readLine(): String? {
429         putBackLine?.let {
430             putBackLine = null
431             return it
432         }
433         val line = super.readLine() ?: return null
434         inText += line
435         if (!skip && markdownPart != MarkdownPart.TOC)
436             outText += line
437         return line
438     }
439 
updateLineAndRetrynull440     fun updateLineAndRetry(line: String) {
441         outText.removeAt(outText.lastIndex)
442         outText += line
443         putBackLine = line
444     }
445 
replaceUntilNextDirectivenull446     fun replaceUntilNextDirective(lines: List<String>): Boolean {
447         skip = true
448         while (true) {
449             val skipLine = readLine() ?: return false
450             if (directive(skipLine) != null) {
451                 putBackLine = skipLine
452                 break
453             }
454         }
455         skip = false
456         outText += lines
457         outText += putBackLine!!
458         return true
459     }
460 }
461 
withLineNumberReadernull462 fun <T : LineNumberReader> File.withLineNumberReader(factory: (Reader) -> T, block: T.() -> Unit): T? {
463     val reader = factory(reader())
464     reader.use {
465         try {
466             it.block()
467         } catch (e: Exception) {
468             println("ERROR: ${this@withLineNumberReader}: ${it.lineNumber}: ${e.message}")
469             return null
470         }
471     }
472     return reader
473 }
474 
withMarkdownTextReadernull475 fun File.withMarkdownTextReader(block: MarkdownTextReader.() -> Unit): MarkdownTextReader? =
476     withLineNumberReader<MarkdownTextReader>(::MarkdownTextReader, block)
477 
478 fun writeLinesIfNeeded(file: File, outLines: List<String>) {
479     val oldLines = try {
480         file.readLines()
481     } catch (e: IOException) {
482         emptyList<String>()
483     }
484     if (outLines != oldLines) writeLines(file, outLines)
485 }
486 
writeLinesnull487 fun writeLines(file: File, lines: List<String>) {
488     println(" Writing $file ...")
489     file.parentFile?.mkdirs()
490     file.printWriter().use { out ->
491         lines.forEach { out.println(it) }
492     }
493 }
494 
findModuleRootDirnull495 fun findModuleRootDir(name: String): String =
496     moduleRoots
497         .map { "$it/$name" }
<lambda>null498         .firstOrNull { File("$it/$moduleMarker").exists() }
499         ?: throw IllegalArgumentException("Module $name is not found in any of the module root dirs")
500 
501 data class ApiIndexKey(
502     val docsRoot: String,
503     val pkg: String
504 )
505 
506 val apiIndexCache: MutableMap<ApiIndexKey, Map<String, List<String>>> = HashMap()
507 
508 val REF_LINE_REGEX = Regex("<a href=\"([a-z0-9_/.\\-]+)\">([a-zA-z0-9.]+)</a>")
509 val INDEX_HTML = "/index.html"
510 val INDEX_MD = "/index.md"
511 val FUNCTIONS_SECTION_HEADER = "### Functions"
512 
putUnambiguousnull513 fun HashMap<String, MutableList<String>>.putUnambiguous(key: String, value: String) {
514     val oldValue = this[key]
515     if (oldValue != null) {
516         oldValue.add(value)
517         put(key, oldValue)
518     } else {
519         put(key, mutableListOf(value))
520     }
521 }
522 
loadApiIndexnull523 fun loadApiIndex(
524     docsRoot: String,
525     path: String,
526     pkg: String,
527     namePrefix: String = ""
528 ): Map<String, MutableList<String>>? {
529     val fileName = "$docsRoot/$path$INDEX_MD"
530     val visited = mutableSetOf<String>()
531     val map = HashMap<String, MutableList<String>>()
532     var inFunctionsSection = false
533     File(fileName).withLineNumberReader(::LineNumberReader) {
534         while (true) {
535             val line = readLine() ?: break
536             if (line == FUNCTIONS_SECTION_HEADER) inFunctionsSection = true
537             val result = REF_LINE_REGEX.matchEntire(line) ?: continue
538             val link = result.groups[1]!!.value
539             if (link.startsWith("..")) continue // ignore cross-references
540             val absLink = "$path/$link"
541             var name = result.groups[2]!!.value
542             // a special disambiguation fix for pseudo-constructor functions
543             if (inFunctionsSection && name[0] in 'A'..'Z') name += "()"
544             val refName = namePrefix + name
545             val fqName = "$pkg.$refName"
546             // Put shorter names for extensions on 3rd party classes (prefix is FQname of those classes)
547             if (namePrefix != "" && namePrefix[0] in 'a'..'z') {
548                 val i = namePrefix.dropLast(1).lastIndexOf('.')
549                 if (i >= 0) map.putUnambiguous(namePrefix.substring(i + 1) + name, absLink)
550                 map.putUnambiguous(name, absLink)
551             }
552             // Disambiguate lower-case names with leading underscore (e.g. Flow class vs flow builder ambiguity)
553             if (namePrefix == "" && name[0] in 'a'..'z') {
554                 map.putUnambiguous("_$name", absLink)
555             }
556             // Always put fully qualified names
557             map.putUnambiguous(refName, absLink)
558             map.putUnambiguous(fqName, absLink)
559             if (link.endsWith(INDEX_HTML)) {
560                 if (visited.add(link)) {
561                     val path2 = path + "/" + link.substring(0, link.length - INDEX_HTML.length)
562                     map += loadApiIndex(docsRoot, path2, pkg, "$refName.")
563                         ?: throw IllegalArgumentException("Failed to parse $docsRoot/$path2")
564                 }
565             }
566         }
567     } ?: return null // return null on failure
568     return map
569 }
570 
processApiIndexnull571 fun processApiIndex(
572     siteRoot: String,
573     docsRoot: String,
574     pkg: String,
575     remainingApiRefNames: MutableSet<String>
576 ): List<String>? {
577     val key = ApiIndexKey(docsRoot, pkg)
578     val map = apiIndexCache.getOrPut(key, {
579         print("Parsing API docs at $docsRoot/$pkg: ")
580         val result = loadApiIndex(docsRoot, pkg, pkg) ?: return null // null on failure
581         println("${result.size} definitions")
582         result
583     })
584     val indexList = arrayListOf<String>()
585     val it = remainingApiRefNames.iterator()
586     while (it.hasNext()) {
587         val refName = it.next()
588         val refLink = map[refName] ?: continue
589         if (refLink.size > 1) {
590             println("INFO: Ambiguous reference to [$refName]: $refLink, taking the shortest one")
591         }
592 
593         val link = refLink.minBy { it.length }
594         indexList += "[$refName]: $siteRoot/$link"
595         it.remove()
596     }
597     return indexList
598 }
599