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
}
}
|