aboutsummaryrefslogtreecommitdiff
path: root/plugins/base
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/base')
-rw-r--r--plugins/base/src/main/kotlin/DokkaBase.kt16
-rw-r--r--plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt11
-rw-r--r--plugins/base/src/main/kotlin/renderers/FileWriter.kt79
-rw-r--r--plugins/base/src/main/kotlin/renderers/OutputWriter.kt7
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt3
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt2
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt2
-rw-r--r--plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt116
-rw-r--r--plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt9
-rw-r--r--plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt99
-rw-r--r--plugins/base/src/main/kotlin/resolvers/LocationProvider.kt19
11 files changed, 353 insertions, 10 deletions
diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt
index 9b6e9b1a..fd842dc0 100644
--- a/plugins/base/src/main/kotlin/DokkaBase.kt
+++ b/plugins/base/src/main/kotlin/DokkaBase.kt
@@ -1,6 +1,11 @@
package org.jetbrains.dokka.base
import org.jetbrains.dokka.CoreExtensions
+import org.jetbrains.dokka.base.renderers.FileWriter
+import org.jetbrains.dokka.base.renderers.OutputWriter
+import org.jetbrains.dokka.base.renderers.html.HtmlRenderer
+import org.jetbrains.dokka.base.resolvers.DefaultLocationProviderFactory
+import org.jetbrains.dokka.base.resolvers.LocationProviderFactory
import org.jetbrains.dokka.base.transformers.descriptors.DefaultDescriptorToDocumentationTranslator
import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentableMerger
import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentablesToPageTranslator
@@ -12,11 +17,12 @@ import org.jetbrains.dokka.base.transformers.pages.merger.PageNodeMerger
import org.jetbrains.dokka.base.transformers.pages.merger.SameMethodNamePageMergerStrategy
import org.jetbrains.dokka.base.transformers.psi.DefaultPsiToDocumentationTranslator
import org.jetbrains.dokka.plugability.DokkaPlugin
-import org.jetbrains.dokka.renderers.html.HtmlRenderer
class DokkaBase : DokkaPlugin() {
val pageMergerStrategy by extensionPoint<PageMergerStrategy>()
val commentsToContentConverter by extensionPoint<CommentsToContentConverter>()
+ val locationproviderFactory by extensionPoint<LocationProviderFactory>()
+ val outputWriter by extensionPoint<OutputWriter>()
val descriptorToDocumentationTranslator by extending(isFallback = true) {
CoreExtensions.descriptorToDocumentationTranslator providing ::DefaultDescriptorToDocumentationTranslator
@@ -57,4 +63,12 @@ class DokkaBase : DokkaPlugin() {
val htmlRenderer by extending {
CoreExtensions.renderer providing ::HtmlRenderer applyIf { format == "html" }
}
+
+ val locationProvider by extending(isFallback = true) {
+ locationproviderFactory providing ::DefaultLocationProviderFactory
+ }
+
+ val fileWriter by extending(isFallback = true) {
+ outputWriter providing ::FileWriter
+ }
} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt
index c6183cf3..951545d2 100644
--- a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt
+++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt
@@ -1,18 +1,19 @@
package org.jetbrains.dokka.base.renderers
-import org.jetbrains.dokka.CoreExtensions
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.resolvers.LocationProvider
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
-import org.jetbrains.dokka.renderers.OutputWriter
+import org.jetbrains.dokka.plugability.plugin
+import org.jetbrains.dokka.plugability.querySingle
import org.jetbrains.dokka.renderers.Renderer
-import org.jetbrains.dokka.resolvers.LocationProvider
import org.jetbrains.dokka.transformers.pages.PageNodeTransformer
abstract class DefaultRenderer<T>(
protected val context: DokkaContext
) : Renderer {
- protected val outputWriter = context.single(CoreExtensions.outputWriter)
+ protected val outputWriter = context.plugin<DokkaBase>().querySingle { outputWriter }
protected lateinit var locationProvider: LocationProvider
private set
@@ -116,7 +117,7 @@ abstract class DefaultRenderer<T>(
val newRoot = preprocessors.fold(root) { acc, t -> t(acc) }
locationProvider =
- context.single(CoreExtensions.locationProviderFactory).getLocationProvider(newRoot)
+ context.plugin<DokkaBase>().querySingle { locationproviderFactory }.getLocationProvider(newRoot)
root.children<ModulePageNode>().forEach { renderPackageList(it) }
diff --git a/plugins/base/src/main/kotlin/renderers/FileWriter.kt b/plugins/base/src/main/kotlin/renderers/FileWriter.kt
new file mode 100644
index 00000000..5d3067fc
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/FileWriter.kt
@@ -0,0 +1,79 @@
+package org.jetbrains.dokka.base.renderers
+
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.renderers.OutputWriter
+import java.io.File
+import java.io.IOException
+import java.net.URI
+import java.nio.file.*
+
+class FileWriter(val context: DokkaContext): OutputWriter {
+ private val createdFiles: MutableSet<String> = mutableSetOf()
+ private val jarUriPrefix = "jar:file:"
+ private val root = context.configuration.outputDir
+
+ override fun write(path: String, text: String, ext: String) {
+ if (createdFiles.contains(path)) {
+ context.logger.error("An attempt to write ${root}/$path several times!")
+ return
+ }
+ createdFiles.add(path)
+
+ try {
+ val dir = Paths.get(root, path.dropLastWhile { it != '/' }).toFile()
+ dir.mkdirsOrFail()
+ Files.write(Paths.get(root, "$path$ext"), text.lines())
+ } catch (e: Throwable) {
+ context.logger.error("Failed to write $this. ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ override fun writeResources(pathFrom: String, pathTo: String) =
+ if (javaClass.getResource(pathFrom).toURI().toString().startsWith(jarUriPrefix)) {
+ copyFromJar(pathFrom, pathTo)
+ } else {
+ copyFromDirectory(pathFrom, pathTo)
+ }
+
+
+ private fun copyFromDirectory(pathFrom: String, pathTo: String) {
+ val dest = Paths.get(root, pathTo).toFile()
+ val uri = javaClass.getResource(pathFrom).toURI()
+ File(uri).copyRecursively(dest, true)
+ }
+
+ private fun copyFromJar(pathFrom: String, pathTo: String) {
+ val rebase = fun(path: String) =
+ "$pathTo/${path.removePrefix(pathFrom)}"
+ val dest = Paths.get(root, pathTo).toFile()
+ dest.mkdirsOrFail()
+ val uri = javaClass.getResource(pathFrom).toURI()
+ val fs = getFileSystemForURI(uri)
+ val path = fs.getPath(pathFrom)
+ for (file in Files.walk(path).iterator()) {
+ if (Files.isDirectory(file)) {
+ val dirPath = file.toAbsolutePath().toString()
+ Paths.get(root, rebase(dirPath)).toFile().mkdirsOrFail()
+ } else {
+ val filePath = file.toAbsolutePath().toString()
+ Paths.get(root, rebase(filePath)).toFile().writeBytes(
+ javaClass.getResourceAsStream(filePath).readBytes()
+ )
+ }
+ }
+ }
+
+ private fun File.mkdirsOrFail() {
+ if (!mkdirs() && !exists()) {
+ throw IOException("Failed to create directory $this")
+ }
+ }
+
+ private fun getFileSystemForURI(uri: URI): FileSystem =
+ try {
+ FileSystems.newFileSystem(uri, emptyMap<String, Any>())
+ } catch (e: FileSystemAlreadyExistsException) {
+ FileSystems.getFileSystem(uri)
+ }
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/OutputWriter.kt b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt
new file mode 100644
index 00000000..a6fda51a
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/OutputWriter.kt
@@ -0,0 +1,7 @@
+package org.jetbrains.dokka.base.renderers
+
+interface OutputWriter {
+
+ fun write(path: String, text: String, ext: String)
+ fun writeResources(pathFrom: String, pathTo: String)
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
index c9270681..8bf00043 100644
--- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
@@ -1,4 +1,4 @@
-package org.jetbrains.dokka.renderers.html
+package org.jetbrains.dokka.base.renderers.html
import kotlinx.html.*
import kotlinx.html.stream.createHTML
@@ -7,7 +7,6 @@ import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.Function
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.plugability.DokkaContext
-import org.jetbrains.dokka.renderers.OutputWriter
import java.io.File
open class HtmlRenderer(
diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt
index 4a2fb40d..ad574769 100644
--- a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt
@@ -1,4 +1,4 @@
-package org.jetbrains.dokka.renderers.html
+package org.jetbrains.dokka.base.renderers.html
import kotlinx.html.*
import kotlinx.html.stream.createHTML
diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt
index 09164d97..ecd2e89a 100644
--- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt
@@ -1,4 +1,4 @@
-package org.jetbrains.dokka.renderers.html
+package org.jetbrains.dokka.base.renderers.html
import kotlinx.html.h1
import kotlinx.html.id
diff --git a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt
new file mode 100644
index 00000000..2238b0c3
--- /dev/null
+++ b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt
@@ -0,0 +1,116 @@
+package org.jetbrains.dokka.base.resolvers
+
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.pages.*
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.utilities.htmlEscape
+import java.util.*
+
+private const val PAGE_WITH_CHILDREN_SUFFIX = "index"
+
+open class DefaultLocationProvider(
+ protected val pageGraphRoot: RootPageNode,
+ protected val dokkaContext: DokkaContext
+) : LocationProvider {
+ protected val extension = ".html"
+
+ protected val pagesIndex: Map<DRI, ContentPage> = pageGraphRoot.asSequence().filterIsInstance<ContentPage>()
+ .map { it.dri.map { dri -> dri to it } }.flatten()
+ .groupingBy { it.first }
+ .aggregate { dri, _, (_, page), first ->
+ if (first) page else throw AssertionError("Multiple pages associated with dri: $dri")
+ }
+
+ protected val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply {
+ fun registerPath(page: PageNode, prefix: List<String>) {
+ val newPrefix = prefix + page.pathName
+ put(page, newPrefix)
+ page.children.forEach { registerPath(it, newPrefix) }
+ }
+ put(pageGraphRoot, emptyList())
+ pageGraphRoot.children.forEach { registerPath(it, emptyList()) }
+ }
+
+ override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean): String =
+ pathTo(node, context) + if (!skipExtension) extension else ""
+
+ override fun resolve(dri: DRI, platforms: List<PlatformData>, context: PageNode?): String =
+ pagesIndex[dri]?.let { resolve(it, context) } ?:
+ // Not found in PageGraph, that means it's an external link
+ ExternalLocationProvider.getLocation(dri,
+ this.dokkaContext.configuration.passesConfigurations
+ .filter { passConfig ->
+ platforms.toSet()
+ .contains(PlatformData(passConfig.moduleName, passConfig.analysisPlatform, passConfig.targets))
+ } // TODO: change targets to something better?
+ .flatMap { it.externalDocumentationLinks }.distinct()
+ )
+
+ override fun resolveRoot(node: PageNode): String =
+ pathTo(pageGraphRoot, node).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 current page graph so it is impossible to compute its path"
+ )
+
+ val contextNode =
+ if (context?.children?.isEmpty() == true && context.parent() != null) context.parent() else context
+ val nodePath = pathFor(node)
+ val contextPath = contextNode?.let { pathFor(it) }.orEmpty()
+
+ val commonPathElements = nodePath.asSequence().zip(contextPath.asSequence())
+ .takeWhile { (a, b) -> a == b }.count()
+
+ return (List(contextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) +
+ if (node.children.isNotEmpty()) listOf(PAGE_WITH_CHILDREN_SUFFIX) else emptyList()).joinToString("/")
+ }
+
+ private fun PageNode.parent() = pageGraphRoot.parentMap[this]
+}
+
+fun DRI.toJavadocLocation(jdkVersion: Int): String { // TODO: classes without packages?
+ val packageLink = packageName?.replace(".", "/")
+ if (classNames == null) {
+ return "$packageLink/package-summary.html".htmlEscape()
+ }
+ val classLink = if (packageLink == null) "$classNames.html" else "$packageLink/$classNames.html"
+ val callableChecked = callable ?: return classLink.htmlEscape()
+
+ val callableLink = "$classLink#${callableChecked.name}" + when {
+ jdkVersion < 8 -> "(${callableChecked.params.joinToString(", ")})"
+ jdkVersion < 10 -> "-${callableChecked.params.joinToString("-")}-"
+ else -> "(${callableChecked.params.joinToString(",")})"
+ }
+
+ return callableLink.htmlEscape()
+}
+
+fun DRI.toDokkaLocation(extension: String): String { // TODO: classes without packages?
+ val classNamesChecked = classNames ?: return "$packageName/index$extension"
+
+ val classLink = if (packageName == null) {
+ ""
+ } else {
+ "$packageName/"
+ } + classNamesChecked.split('.').joinToString("/", transform = ::identifierToFilename)
+
+ val callableChecked = callable ?: return "$classLink/index$extension"
+
+ return "$classLink/${identifierToFilename(callableChecked.name)}$extension"
+}
+
+private val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out")
+
+private fun identifierToFilename(name: String): String {
+ if (name.isEmpty()) return "--root--"
+ val escaped = name.replace('<', '-').replace('>', '-')
+ val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() }
+ return if (lowercase in reservedFilenames) "--$lowercase--" else lowercase
+}
+
+private val PageNode.pathName: String
+ get() = if (this is PackagePageNode) name else identifierToFilename(name)
diff --git a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt
new file mode 100644
index 00000000..c649e22b
--- /dev/null
+++ b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt
@@ -0,0 +1,9 @@
+package org.jetbrains.dokka.base.resolvers
+
+import org.jetbrains.dokka.pages.RootPageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+
+class DefaultLocationProviderFactory(private val context: DokkaContext) : LocationProviderFactory {
+
+ override fun getLocationProvider(pageNode: RootPageNode) = DefaultLocationProvider(pageNode, context)
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt
new file mode 100644
index 00000000..7c0e9952
--- /dev/null
+++ b/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt
@@ -0,0 +1,99 @@
+package org.jetbrains.dokka.base.resolvers
+
+import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink
+import org.jetbrains.dokka.links.DRI
+import java.net.HttpURLConnection
+import java.net.URL
+import java.net.URLConnection
+
+object ExternalLocationProvider { // TODO: Refactor this!!!
+ private const val DOKKA_PARAM_PREFIX = "\$dokka."
+
+ private val cache: MutableMap<URL, LocationInfo> = mutableMapOf()
+
+ fun getLocation(dri: DRI, externalDocumentationLinks: List<ExternalDocumentationLink>): String {
+ val toResolve: MutableList<ExternalDocumentationLink> = mutableListOf()
+ for(link in externalDocumentationLinks){
+ val info = cache[link.packageListUrl]
+ if(info == null) {
+ toResolve.add(link)
+ } else if(info.packages.contains(dri.packageName)) {
+ return link.url.toExternalForm() + getLink(dri, info)
+ }
+ }
+ // Not in cache, resolve packageLists
+ while (toResolve.isNotEmpty()){
+ val link = toResolve.first().also { toResolve.remove(it) }
+ val locationInfo = loadPackageList(link.packageListUrl)
+ if(locationInfo.packages.contains(dri.packageName)) {
+ return link.url.toExternalForm() + getLink(dri, locationInfo)
+ }
+ }
+ return ""
+ }
+
+ private fun getLink(dri: DRI, locationInfo: LocationInfo): String = when(locationInfo.format) {
+ "javadoc" -> dri.toJavadocLocation(8)
+ "kotlin-website-html", "html" -> locationInfo.locations[dri.packageName + "." + dri.classNames] ?: dri.toDokkaLocation(".html")
+ "markdown" -> locationInfo.locations[dri.packageName + "." + dri.classNames] ?: dri.toDokkaLocation(".md")
+ // TODO: rework this
+ else -> throw RuntimeException("Unrecognized format")
+ }
+
+
+ private fun loadPackageList(url: URL): LocationInfo {
+ val packageListStream = url.doOpenConnectionToReadContent().getInputStream()
+ val (params, packages) =
+ packageListStream
+ .bufferedReader()
+ .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } }
+
+ val paramsMap = params.asSequence()
+ .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) }
+ .groupBy({ (key, _) -> key }, { (_, value) -> value })
+
+ val format = paramsMap["format"]?.singleOrNull() ?: "javadoc"
+
+ val locations = paramsMap["location"].orEmpty()
+ .map { it.split("\u001f", limit = 2) }
+ .map { (key, value) -> key to value }
+ .toMap()
+
+ val info = LocationInfo(format, packages.toSet(), locations)
+ cache[url] = info
+ return info
+ }
+
+ private fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection {
+ val connection = this.openConnection().apply {
+ connectTimeout = timeout
+ readTimeout = timeout
+ }
+
+ when (connection) {
+ is HttpURLConnection -> {
+ return when (connection.responseCode) {
+ in 200..299 -> {
+ connection
+ }
+ HttpURLConnection.HTTP_MOVED_PERM,
+ HttpURLConnection.HTTP_MOVED_TEMP,
+ HttpURLConnection.HTTP_SEE_OTHER -> {
+ if (redirectsAllowed > 0) {
+ val newUrl = connection.getHeaderField("Location")
+ URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1)
+ } else {
+ throw RuntimeException("Too many redirects")
+ }
+ }
+ else -> {
+ throw RuntimeException("Unhandled http code: ${connection.responseCode}")
+ }
+ }
+ }
+ else -> return connection
+ }
+ }
+ data class LocationInfo(val format: String, val packages: Set<String>, val locations: Map<String, String>)
+
+}
diff --git a/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt
new file mode 100644
index 00000000..13f4563c
--- /dev/null
+++ b/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt
@@ -0,0 +1,19 @@
+package org.jetbrains.dokka.base.resolvers
+
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.pages.ContentPage
+import org.jetbrains.dokka.pages.PageNode
+import org.jetbrains.dokka.pages.PlatformData
+import org.jetbrains.dokka.pages.RootPageNode
+
+interface LocationProvider {
+ fun resolve(dri: DRI, platforms: List<PlatformData>, context: PageNode? = null): String
+ fun resolve(node: PageNode, context: PageNode? = null, skipExtension: Boolean = false): String
+ fun resolveRoot(node: PageNode): String
+ fun ancestors(node: PageNode): List<PageNode>
+}
+
+interface LocationProviderFactory {
+ fun getLocationProvider(pageNode: RootPageNode): LocationProvider
+}
+