From c09bde34ff729ef9b1f3bea602fb53cd4e6dca42 Mon Sep 17 00:00:00 2001 From: Andrzej Ratajczak Date: Fri, 13 Mar 2020 10:46:33 +0100 Subject: Gradle Task supporting multimodular projects --- plugins/base/src/main/kotlin/DokkaBase.kt | 15 +- .../src/main/kotlin/renderers/DefaultRenderer.kt | 2 +- .../kotlin/resolvers/DefaultLocationProvider.kt | 116 ----------- .../resolvers/DefaultLocationProviderFactory.kt | 9 - .../kotlin/resolvers/ExternalLocationProvider.kt | 99 --------- .../src/main/kotlin/resolvers/LocationProvider.kt | 19 -- .../DokkaExternalLocationProviderFactory.kt | 35 ++++ .../external/ExternalLocationProviderFactory.kt | 23 +++ .../JavadocExternalLocationProviderFactory.kt | 37 ++++ .../resolvers/local/DefaultLocationProvider.kt | 222 +++++++++++++++++++++ .../local/DefaultLocationProviderFactory.kt | 10 + .../kotlin/resolvers/local/LocationProvider.kt | 18 ++ .../documentables/DefaultDocumentableMerger.kt | 12 +- .../documentables/DocumentableVisibilityFilter.kt | 1 + .../pages/comments/DocTagToContentConverter.kt | 7 +- .../test/kotlin/renderers/RenderingOnlyTestBase.kt | 12 +- 16 files changed, 378 insertions(+), 259 deletions(-) delete mode 100644 plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt delete mode 100644 plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt delete mode 100644 plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt delete mode 100644 plugins/base/src/main/kotlin/resolvers/LocationProvider.kt create mode 100644 plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt create mode 100644 plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt create mode 100644 plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt create mode 100644 plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt create mode 100644 plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt create mode 100644 plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt (limited to 'plugins/base/src') diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index 448373ea..548bcb93 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -4,10 +4,12 @@ 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.* -import org.jetbrains.dokka.base.resolvers.DefaultLocationProviderFactory -import org.jetbrains.dokka.base.resolvers.LocationProviderFactory +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer import org.jetbrains.dokka.base.signatures.KotlinSignatureProvider import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.resolvers.external.* +import org.jetbrains.dokka.base.resolvers.local.DefaultLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentableMerger import org.jetbrains.dokka.base.transformers.documentables.InheritorsExtractorTransformer import org.jetbrains.dokka.base.transformers.pages.annotations.DeprecatedStrikethroughTransformer @@ -29,6 +31,7 @@ class DokkaBase : DokkaPlugin() { val commentsToContentConverter by extensionPoint() val signatureProvider by extensionPoint() val locationProviderFactory by extensionPoint() + val externalLocationProviderFactory by extensionPoint() val outputWriter by extensionPoint() val htmlPreprocessors by extensionPoint() @@ -98,6 +101,14 @@ class DokkaBase : DokkaPlugin() { locationProviderFactory providing ::DefaultLocationProviderFactory } + val javadocLocationProvider by extending { + externalLocationProviderFactory with JavadocExternalLocationProviderFactory() + } + + val dokkaLocationProvider by extending { + externalLocationProviderFactory with DokkaExternalLocationProviderFactory() + } + val fileWriter by extending(isFallback = true) { outputWriter providing ::FileWriter } diff --git a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt index 0e0001b9..0ff0511a 100644 --- a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt @@ -1,7 +1,7 @@ package org.jetbrains.dokka.base.renderers import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.resolvers.LocationProvider +import org.jetbrains.dokka.base.resolvers.local.LocationProvider import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin diff --git a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt deleted file mode 100644 index 2238b0c3..00000000 --- a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProvider.kt +++ /dev/null @@ -1,116 +0,0 @@ -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 = pageGraphRoot.asSequence().filterIsInstance() - .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> = IdentityHashMap>().apply { - fun registerPath(page: PageNode, prefix: List) { - 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, 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 = - 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 deleted file mode 100644 index c649e22b..00000000 --- a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 7c0e9952..00000000 --- a/plugins/base/src/main/kotlin/resolvers/ExternalLocationProvider.kt +++ /dev/null @@ -1,99 +0,0 @@ -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 = mutableMapOf() - - fun getLocation(dri: DRI, externalDocumentationLinks: List): String { - val toResolve: MutableList = 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, val locations: Map) - -} diff --git a/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt deleted file mode 100644 index 13f4563c..00000000 --- a/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt +++ /dev/null @@ -1,19 +0,0 @@ -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, 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 -} - -interface LocationProviderFactory { - fun getLocationProvider(pageNode: RootPageNode): LocationProvider -} - diff --git a/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt new file mode 100644 index 00000000..ff9186f7 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt @@ -0,0 +1,35 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.local.identifierToFilename +import org.jetbrains.dokka.links.DRI + + +class DokkaExternalLocationProviderFactory : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + object : ExternalLocationProviderFactory { + override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = + when (param) { + "kotlin-website-html", "html" -> DokkaExternalLocationProvider(param, ".html") + "markdown" -> DokkaExternalLocationProvider(param, ".md") + else -> null + } + } +) + +class DokkaExternalLocationProvider(override val param: String, val extension: String) : ExternalLocationProvider { + + override fun DRI.toLocation(): String { // TODO: classes without packages? + + val classNamesChecked = classNames ?: return "${packageName ?: ""}/index$extension" + + val classLink = (listOfNotNull(packageName) + classNamesChecked.split('.')).joinToString( + "/", + transform = ::identifierToFilename + ) + + val callableChecked = callable ?: return "$classLink/index$extension" + + return "$classLink/${identifierToFilename( + callableChecked.name + )}$extension" + } +} diff --git a/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt new file mode 100644 index 00000000..6fb05024 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt @@ -0,0 +1,23 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.links.DRI + + +interface ExternalLocationProvider { + + val param: String + fun DRI.toLocation(): String +} + +interface ExternalLocationProviderFactory { + + fun getExternalLocationProvider(param: String): ExternalLocationProvider? +} + +class ExternalLocationProviderFactoryWithCache(val ext: ExternalLocationProviderFactory) : ExternalLocationProviderFactory { + + private val locationProviders: MutableList = mutableListOf() + + override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = + locationProviders.find { it.param == param } ?: ext.getExternalLocationProvider(param)?.also { locationProviders.add(it) } +} \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt new file mode 100644 index 00000000..c52c9bbb --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt @@ -0,0 +1,37 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.utilities.htmlEscape + +class JavadocExternalLocationProviderFactory : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + object : ExternalLocationProviderFactory { + override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = + when(param) { + "javadoc1" -> JavadocExternalLocationProvider(param, "()", ", ") // Covers JDK 1 - 7 + "javadoc8" -> JavadocExternalLocationProvider(param, "--", "-") // Covers JDK 8 - 9 + "javadoc10" -> JavadocExternalLocationProvider(param, "()", ",") // Covers JDK 10 + else -> null + } + } +) + +class JavadocExternalLocationProvider(override val param: String, val brackets: String, val separator: String) : ExternalLocationProvider { + + override fun DRI.toLocation(): String { + + 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 + + "${brackets.first()}" + + callableChecked.params.joinToString(separator) + + "${brackets.last()}" + + return callableLink.htmlEscape() + } +} \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt new file mode 100644 index 00000000..736367a9 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt @@ -0,0 +1,222 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProvider +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import java.lang.IllegalStateException +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.util.* + +private const val PAGE_WITH_CHILDREN_SUFFIX = "index" +private const val DOKKA_PARAM_PREFIX = "\$dokka." + +open class DefaultLocationProvider( + protected val pageGraphRoot: RootPageNode, + protected val dokkaContext: DokkaContext +) : LocationProvider { + protected val extension = ".html" + + protected val externalLocationProviderFactories = + dokkaContext.plugin().query { externalLocationProviderFactory } + + protected val pagesIndex: Map = pageGraphRoot.asSequence().filterIsInstance() + .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> = IdentityHashMap>().apply { + fun registerPath(page: PageNode, prefix: List) { + 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, context: PageNode?): String = + pagesIndex[dri]?.let { resolve(it, context) } ?: + // Not found in PageGraph, that means it's an external link + getLocation(dri, + this.dokkaContext.configuration.passesConfigurations + .filter { passConfig -> + platforms.toSet() + .contains(PlatformData(passConfig.moduleName, passConfig.analysisPlatform, passConfig.targets)) + } // TODO: change targets to something better? + .groupBy({ it.jdkVersion }, { it.externalDocumentationLinks }) + .map { it.key to it.value.flatten().distinct() }.toMap() + ) + + override fun resolveRoot(node: PageNode): String = + pathTo(pageGraphRoot, node).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 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] + + private val cache: MutableMap = mutableMapOf() + + private fun getLocation( + dri: DRI, + jdkToExternalDocumentationLinks: Map> + ): String { + val toResolve: MutableMap> = mutableMapOf() + for ((jdk, links) in jdkToExternalDocumentationLinks) { + for (link in links) { + val info = cache[link.packageListUrl] + if (info == null) { + toResolve.getOrPut(jdk) { mutableListOf() }.add(link) + } else if (info.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink( + dri, + info + ) + } + } + } + // Not in cache, resolve packageLists + for ((jdk, links) in toResolve) { + for (link in links) { + val locationInfo = + loadPackageList( + jdk, + link.packageListUrl + ) + if (locationInfo.packages.contains(dri.packageName)) { + return link.url.toExternalForm() + getLink( + dri, + locationInfo + ) + } + } + toResolve.remove(jdk) + } + return "" + } + + private fun getLink(dri: DRI, locationInfo: LocationInfo): String = + locationInfo.locations[dri.packageName + "." + dri.classNames] + ?: // Not sure if it can be here, previously it shadowed only kotlin/dokka related sources, here it shadows both dokka/javadoc, cause I cannot distinguish what LocationProvider has been hypothetically chosen + if (locationInfo.externalLocationProvider != null) + with(locationInfo.externalLocationProvider) { + dri.toLocation() + } + else + throw IllegalStateException("Have not found any convenient ExternalLocationProvider for $dri DRI!") + + private fun loadPackageList(jdk: Int, 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() ?: when { + jdk < 8 -> "javadoc1" // Covers JDK 1 - 7 + jdk < 10 -> "javadoc8" // Covers JDK 8 - 9 + else -> "javadoc10" // Covers JDK 10+ + } + + val locations = paramsMap["location"].orEmpty() + .map { it.split("\u001f", limit = 2) } + .map { (key, value) -> key to value } + .toMap() + + val externalLocationProvider = + externalLocationProviderFactories.asSequence().map { it.getExternalLocationProvider(format) } + .filterNotNull().take(1).firstOrNull() + + val info = LocationInfo( + externalLocationProvider, + 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 externalLocationProvider: ExternalLocationProvider?, + val packages: Set, + val locations: Map + ) +} + +private val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out") + +internal fun identifierToFilename(name: String): String { + if (name.isEmpty()) return "--root--" + val escaped = name.replace("<|>".toRegex(), "-") + 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/local/DefaultLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt new file mode 100644 index 00000000..57f53ba6 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.base.resolvers.local + +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/local/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt new file mode 100644 index 00000000..0814cb3e --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.links.DRI +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, 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 +} + +interface LocationProviderFactory { + fun getLocationProvider(pageNode: RootPageNode): LocationProvider +} + diff --git a/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt b/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt index bb7dbaaf..48be8ae7 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt @@ -12,17 +12,17 @@ import org.jetbrains.dokka.transformers.documentation.DocumentableMerger import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult internal object DefaultDocumentableMerger : DocumentableMerger { + override fun invoke(modules: Collection, context: DokkaContext): DModule { - val name = modules.map { it.name }.distinct().singleOrNull() ?: run { - context.logger.error("All module names need to be the same") - modules.first().name - } + + val projectName = + modules.fold(modules.first().name) { acc, module -> acc.commonPrefixWith(module.name) }.takeIf { it.isNotEmpty() } + ?: "project" return modules.reduce { left, right -> val list = listOf(left, right) - DModule( - name = name, + name = projectName, packages = merge( list.flatMap { it.packages }, DPackage::mergeWith diff --git a/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilter.kt b/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilter.kt index 150df302..9f86c82a 100644 --- a/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilter.kt +++ b/plugins/base/src/main/kotlin/transformers/documentables/DocumentableVisibilityFilter.kt @@ -15,6 +15,7 @@ internal object DocumentableVisibilityFilter : PreMergeDocumentableTransformer { val packageOptions = context.configuration.passesConfigurations.first { original.platformData.contains(it.platformData) } .perPackageOptions + DocumentableFilter(packageOptions).processModule(original) } diff --git a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt index 885fbfee..2eb63504 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/comments/DocTagToContentConverter.kt @@ -30,14 +30,15 @@ object DocTagToContentConverter : CommentsToContentConverter { ) ) - fun buildList(ordered: Boolean) = + fun buildList(ordered: Boolean, start: Int = 1) = listOf( ContentList( buildChildren(docTag), ordered, dci, platforms, - styles + styles, + ((PropertyContainer.empty()) + SimpleAttr("start", start.toString())) ) ) @@ -53,7 +54,7 @@ object DocTagToContentConverter : CommentsToContentConverter { is H5 -> buildHeader(5) is H6 -> buildHeader(6) is Ul -> buildList(false) - is Ol -> buildList(true) + is Ol -> buildList(true, docTag.params["start"]?.toInt() ?: 1) is Li -> buildChildren(docTag) is Br -> buildNewLine() is B -> buildChildren(docTag, setOf(TextStyle.Strong)) diff --git a/plugins/base/src/test/kotlin/renderers/RenderingOnlyTestBase.kt b/plugins/base/src/test/kotlin/renderers/RenderingOnlyTestBase.kt index f77ec086..852ac735 100644 --- a/plugins/base/src/test/kotlin/renderers/RenderingOnlyTestBase.kt +++ b/plugins/base/src/test/kotlin/renderers/RenderingOnlyTestBase.kt @@ -2,9 +2,11 @@ package renderers import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.renderers.html.RootCreator -import org.jetbrains.dokka.base.resolvers.DefaultLocationProviderFactory -import org.jetbrains.dokka.base.resolvers.LocationProvider -import org.jetbrains.dokka.base.resolvers.LocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.DokkaExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.JavadocExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.DefaultLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory import org.jetbrains.dokka.base.signatures.KotlinSignatureProvider import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder @@ -26,7 +28,9 @@ abstract class RenderingOnlyTestBase { val context = MockContext( DokkaBase().outputWriter to { _ -> files }, DokkaBase().locationProviderFactory to ::DefaultLocationProviderFactory, - DokkaBase().htmlPreprocessors to { _ -> RootCreator } + DokkaBase().htmlPreprocessors to { _ -> RootCreator }, + DokkaBase().externalLocationProviderFactory to { _ -> ::JavadocExternalLocationProviderFactory }, + DokkaBase().externalLocationProviderFactory to { _ -> ::DokkaExternalLocationProviderFactory } ) protected val renderedContent: Element by lazy { -- cgit