aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin/renderers/html/SearchbarDataInstaller.kt
blob: 6ef6a6ec3fd2519ffdacf59e0a756c182442c075 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package org.jetbrains.dokka.base.renderers.html

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.jetbrains.dokka.Platform
import org.jetbrains.dokka.model.DisplaySourceSet
import org.jetbrains.dokka.model.dfs
import org.jetbrains.dokka.model.withDescendants
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.transformers.pages.PageTransformer
import java.util.concurrent.ConcurrentHashMap

typealias PageId = String
typealias Json = String

data class SearchRecord(
    val name: String,
    val description: String? = null,
    val location: String,
    val searchKeys: List<String> = listOf(name)
) {
    companion object {}
}

open class SearchbarDataInstaller(val context: DokkaContext) : PageTransformer {
    data class PageWithId(val id: PageId, val page: ContentPage) {
        val displayableSignature = getSymbolSignature(page)?.let { flattenToText(it) } ?: page.name
    }

    private val mapper = jacksonObjectMapper()

    open fun generatePagesList(pages: Map<PageId, PageWithId>, locationResolver: PageResolver): Json =
        pages.entries
            .filter { it.key.isNotEmpty() }
            .sortedWith(compareBy({ it.key }, { it.value.displayableSignature }))
            .groupBy { it.key.substringAfterLast(".") }
            .entries
            .flatMap { entry ->
                entry.value.map { subentry ->
                    createSearchRecord(
                        name = subentry.value.displayableSignature,
                        description = subentry.key,
                        location = resolveLocation(locationResolver, subentry.value.page).orEmpty(),
                        searchKeys = listOf(entry.key, subentry.value.displayableSignature)
                    )
                }
            }.run { mapper.writeValueAsString(this) }

    open fun createSearchRecord(
        name: String,
        description: String?,
        location: String,
        searchKeys: List<String>
    ): SearchRecord =
        SearchRecord(name, description, location, searchKeys)

    open fun processPage(page: PageNode): PageWithId? =
        when (page) {
            is ContentPage -> page.takeIf { page !is ModulePageNode && page !is PackagePageNode }?.documentable
                ?.let { documentable ->
                    listOfNotNull(
                        documentable.dri.packageName,
                        documentable.dri.classNames,
                        documentable.dri.callable?.name
                    ).takeIf { it.isNotEmpty() }?.joinToString(".")
                }?.let { id ->
                    PageWithId(id, page)
                }
            else -> null
        }

    private fun resolveLocation(locationResolver: PageResolver, page: ContentPage): String? =
        locationResolver(page, null).also { location ->
            if (location.isNullOrBlank()) context.logger.warn("Cannot resolve path for ${page.dri}")
        }

    override fun invoke(input: RootPageNode): RootPageNode {
        val page = RendererSpecificResourcePage(
            name = "scripts/pages.js",
            children = emptyList(),
            strategy = RenderingStrategy.PageLocationResolvableWrite { resolver ->
                input.withDescendants().fold(emptyMap<PageId, PageWithId>()) { pageList, page ->
                    processPage(page)?.let { pageList + Pair(it.id, it) } ?: pageList
                }.run {
                    """var pages = ${generatePagesList(this, resolver)}"""
                }
            })

        return input.modified(children = input.children + page)
    }
}

private fun getSymbolSignature(page: ContentPage) = page.content.dfs { it.dci.kind == ContentKind.Symbol }

private fun flattenToText(node: ContentNode): String {
    fun getContentTextNodes(node: ContentNode, sourceSetRestriction: DisplaySourceSet): List<ContentText> =
        when (node) {
            is ContentText -> listOf(node)
            is ContentComposite -> node.children
                .filter { sourceSetRestriction in it.sourceSets }
                .flatMap { getContentTextNodes(it, sourceSetRestriction) }
                .takeIf { node.dci.kind != ContentKind.Annotations }
                .orEmpty()
            else -> emptyList()
        }

    val sourceSetRestriction =
        node.sourceSets.find { it.platform == Platform.common } ?: node.sourceSets.first()
    return getContentTextNodes(node, sourceSetRestriction).joinToString("") { it.text }
}