aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt
blob: 4934e8f4870069aca5ed77e254e8c9215a8586a6 (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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package org.jetbrains.dokka.base.resolvers.local

import org.jetbrains.dokka.base.renderers.sourceSets
import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.links.PointingToDeclaration
import org.jetbrains.dokka.model.DisplaySourceSet
import org.jetbrains.dokka.model.sourceSetIDs
import org.jetbrains.dokka.model.toDisplaySourceSet
import org.jetbrains.dokka.model.withDescendants
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import java.util.*

open class DokkaLocationProvider(
    pageGraphRoot: RootPageNode,
    dokkaContext: DokkaContext,
    val extension: String = ".html"
) : DokkaBaseLocationProvider(pageGraphRoot, dokkaContext) {
    protected open val PAGE_WITH_CHILDREN_SUFFIX = "index"

    protected open val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply {
        fun registerPath(page: PageNode, prefix: List<String>) {
            if (page is RootPageNode && page.forceTopLevelName) {
                put(page, prefix + PAGE_WITH_CHILDREN_SUFFIX)
                page.children.forEach { registerPath(it, prefix) }
            } else {
                val newPrefix = prefix + page.pathName
                put(page, if (page is ModulePageNode) prefix else newPrefix)
                page.children.forEach { registerPath(it, newPrefix) }
            }

        }
        put(pageGraphRoot, emptyList())
        pageGraphRoot.children.forEach { registerPath(it, emptyList()) }
    }

    protected val pagesIndex: Map<DRIWithSourceSets, ContentPage> =
        pageGraphRoot.withDescendants().filterIsInstance<ContentPage>()
            .flatMap { page ->
                page.dri.flatMap { dri ->
                    page.sourceSets().ifEmpty { setOf(null) }
                        .map { sourceSet -> DRIWithSourceSets(dri, setOfNotNull(sourceSet)) to page }
                        .let {
                            if (it.size > 1) {
                                it + (DRIWithSourceSets(dri, page.sourceSets()) to page)
                            } else {
                                it
                            }
                        }
                }
            }
            .groupingBy { it.first }
            .aggregate { key, _, (_, page), first ->
                if (first) page else throw AssertionError("Multiple pages associated with key: ${key.dri}/${key.sourceSet}")
            }

    protected val anchorsIndex: Map<DRIWithSourceSets, PageWithKind> =
        pageGraphRoot.withDescendants().filterIsInstance<ContentPage>()
            .flatMap { page ->
                page.content.withDescendants()
                    .filter { it.extra[SymbolAnchorHint] != null && it.dci.dri.any() }
                    .flatMap { content ->
                        content.dci.dri.map { dri ->
                            (dri to content.sourceSets) to content.extra[SymbolAnchorHint]?.contentKind!!
                        }
                    }
                    .distinct()
                    .flatMap { (pair, kind) ->
                        val (dri, sourceSets) = pair
                        sourceSets.ifEmpty { setOf(null) }.map { sourceSet ->
                            DRIWithSourceSets(dri, setOfNotNull(sourceSet)) to PageWithKind(page, kind)
                        }
                    }
            }.toMap()

    override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean) =
        pathTo(node, context) + if (!skipExtension) extension else ""

    override fun resolve(dri: DRI, sourceSets: Set<DisplaySourceSet>, context: PageNode?): String? =
        sourceSets.ifEmpty { setOf(null) }.mapNotNull { sourceSet ->
            val driWithSourceSets = DRIWithSourceSets(dri, setOfNotNull(sourceSet))
            getLocalLocation(driWithSourceSets, context)
                ?: getLocalLocation(driWithSourceSets.copy(dri = dri.copy(target = PointingToDeclaration)), context)
                // Not found in PageGraph, that means it's an external link
                ?: getExternalLocation(dri, sourceSets)
                ?: getExternalLocation(dri.copy(target = PointingToDeclaration), sourceSets)
        }.distinct().singleOrNull()

    private fun getLocalLocation(driWithSourceSets: DRIWithSourceSets, context: PageNode?): String? {
        val (dri, originalSourceSet) = driWithSourceSets
        val allSourceSets: List<Set<DisplaySourceSet>> =
            listOf(originalSourceSet) + originalSourceSet.let { oss ->
                dokkaContext.configuration.sourceSets.filter { it.sourceSetID in oss.sourceSetIDs }
                    .flatMap { it.dependentSourceSets }
                    .mapNotNull { ssid ->
                        dokkaContext.configuration.sourceSets.find { it.sourceSetID == ssid }?.toDisplaySourceSet()
                    }.map {
                        // be careful `data DisplaySourceSet: Set<DisplaySourceSet>` but `setOf(someDisplaySourceSet) != someDisplaySourceSet`
                        setOf(it)
                    }
            }

        return getLocalPageLink(dri, allSourceSets, context)
            ?: getLocalAnchor(dri, allSourceSets, context)
    }

    private fun getLocalPageLink(dri: DRI, allSourceSets: Iterable<Set<DisplaySourceSet>>, context: PageNode?)  =
        allSourceSets.mapNotNull { displaySourceSet ->
            pagesIndex[DRIWithSourceSets(dri, displaySourceSet)]
        }.firstOrNull()?.let { page -> resolve(page, context) }

    private fun getLocalAnchor(dri: DRI, allSourceSets: Iterable<Set<DisplaySourceSet>>, context: PageNode?)  =
        allSourceSets.mapNotNull { displaySourceSet ->
            anchorsIndex[DRIWithSourceSets(dri, displaySourceSet)]?.let { (page, kind) ->
                val dci = DCI(setOf(dri), kind)
                resolve(page, context) + "#" + anchorForDCI(dci, displaySourceSet)
            }
        }.firstOrNull()

    override fun pathToRoot(from: PageNode): String =
        pathTo(pageGraphRoot, from).removeSuffix(PAGE_WITH_CHILDREN_SUFFIX)

    override fun ancestors(node: PageNode): List<PageNode> =
        generateSequence(node) { it.parent() }.toList()

    protected open fun pathTo(node: PageNode, context: PageNode?): String {
        fun pathFor(page: PageNode) = pathsIndex[page] ?: throw AssertionError(
            "${page::class.simpleName}(${page.name}) does not belong to the current page graph so it is impossible to compute its path"
        )

        val nodePath = pathFor(node)
        val contextPath = context?.let { pathFor(it) }.orEmpty()
        val endedContextPath = if (context?.isIndexPage() == false)
            contextPath.toMutableList().also { it.removeLastOrNull() }
        else contextPath

        val commonPathElements = nodePath.asSequence().zip(endedContextPath.asSequence())
            .takeWhile { (a, b) -> a == b }.count()

        return (List(endedContextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) +
                if (node.isIndexPage())
                    listOf(PAGE_WITH_CHILDREN_SUFFIX)
                else
                    emptyList()
                ).joinToString("/")
    }

    private fun PageNode.isIndexPage() = this is ClasslikePageNode || children.isNotEmpty()

    private fun PageNode.parent() = pageGraphRoot.parentMap[this]

    private val PageNode.pathName: String
        get() = if (this is PackagePageNode || this is RendererSpecificResourcePage) name else identifierToFilename(name)

    protected data class DRIWithSourceSets(val dri: DRI, val sourceSet: Set<DisplaySourceSet>)

    protected data class PageWithKind(val page: ContentPage, val kind: Kind)

    companion object {
        val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out")

        //Taken from: https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
        internal val reservedCharacters = setOf('|', '>', '<', '*', ':', '"', '?', '%')

        fun identifierToFilename(name: String): String {
            if (name.isEmpty()) return "--root--"
            return sanitizeFileName(name, reservedFilenames, reservedCharacters)
        }
    }
}

internal fun sanitizeFileName(name: String, reservedFileNames: Set<String>, reservedCharacters: Set<Char>): String {
    val lowercase = name.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() }
    val withoutReservedFileNames = if (lowercase in reservedFileNames) "--$lowercase--" else lowercase
    return reservedCharacters.fold(withoutReservedFileNames) { acc, character ->
        if (character in acc) acc.replace(character.toString(), "[${character.toInt()}]")
        else acc
    }
}