org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dokka-base Show documentation
Show all versions of dokka-base Show documentation
Dokka is an API documentation engine for Kotlin
The newest version!
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
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.base.pages.AllTypesPageNode
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.links.PointingToDeclaration
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
import java.util.*
public open class DokkaLocationProvider(
pageGraphRoot: RootPageNode,
dokkaContext: DokkaContext,
public val extension: String = ".html"
) : DokkaBaseLocationProvider(pageGraphRoot, dokkaContext) {
protected open val PAGE_WITH_CHILDREN_SUFFIX: String = "index"
protected open val pathsIndex: Map> = IdentityHashMap>().apply {
fun registerPath(page: PageNode, prefix: List) {
if (page is RootPageNode && page.forceTopLevelName) {
put(page, prefix + PAGE_WITH_CHILDREN_SUFFIX)
page.children.forEach { registerPath(it, prefix) }
} else if (page is AllTypesPageNode) {
put(page, prefix + ALL_TYPES_PAGE_PATH)
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 =
pageGraphRoot.withDescendants().filterIsInstance()
.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 =
pageGraphRoot.withDescendants().filterIsInstance()
.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): String =
pathTo(node, context) + if (!skipExtension) extension else ""
override fun resolve(dri: DRI, sourceSets: Set, 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> =
listOf(originalSourceSet) + originalSourceSet.let { oss ->
val ossIds = oss.computeSourceSetIds()
dokkaContext.configuration.sourceSets.filter { it.sourceSetID in ossIds }
.flatMap { it.dependentSourceSets }
.mapNotNull { ssid ->
dokkaContext.configuration.sourceSets.find { it.sourceSetID == ssid }?.toDisplaySourceSet()
}.map {
setOf(it)
}
}
return getLocalPageLink(dri, allSourceSets, context)
?: getLocalAnchor(dri, allSourceSets, context)
}
private fun getLocalPageLink(dri: DRI, allSourceSets: Iterable>, context: PageNode?) =
allSourceSets.mapNotNull { displaySourceSet ->
pagesIndex[DRIWithSourceSets(dri, displaySourceSet)]
}.firstOrNull()?.let { page -> resolve(page, context) }
private fun getLocalAnchor(dri: DRI, allSourceSets: Iterable>, 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 =
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)
protected data class PageWithKind(val page: ContentPage, val kind: Kind)
public companion object {
private const val ALL_TYPES_PAGE_PATH: String = "all-types"
public val reservedFilenames: Set = setOf(
"index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out",
ALL_TYPES_PAGE_PATH
)
//Taken from: https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
internal val reservedCharacters = setOf('|', '>', '<', '*', ':', '"', '?', '%')
public fun identifierToFilename(name: String): String {
if (name.isEmpty()) return "--root--"
return sanitizeFileName(name, reservedFilenames, reservedCharacters)
}
}
}
internal fun sanitizeFileName(name: String, reservedFileNames: Set, reservedCharacters: Set): 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
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy