diff options
author | Andrzej Ratajczak <andrzej.ratajczak98@gmail.com> | 2020-03-13 10:46:33 +0100 |
---|---|---|
committer | Błażej Kardyś <bkardys@virtuslab.com> | 2020-03-23 13:57:07 +0100 |
commit | c09bde34ff729ef9b1f3bea602fb53cd4e6dca42 (patch) | |
tree | 3a744a16c133b813ab1849761aef1ec3544f401f | |
parent | cedf8b7594deef2cd26e981865daa8aae0155520 (diff) | |
download | dokka-c09bde34ff729ef9b1f3bea602fb53cd4e6dca42.tar.gz dokka-c09bde34ff729ef9b1f3bea602fb53cd4e6dca42.tar.bz2 dokka-c09bde34ff729ef9b1f3bea602fb53cd4e6dca42.zip |
Gradle Task supporting multimodular projects
29 files changed, 898 insertions, 282 deletions
diff --git a/core/src/main/kotlin/DokkaBootstrapImpl.kt b/core/src/main/kotlin/DokkaBootstrapImpl.kt index fc1b6926..f164a3c1 100644 --- a/core/src/main/kotlin/DokkaBootstrapImpl.kt +++ b/core/src/main/kotlin/DokkaBootstrapImpl.kt @@ -20,7 +20,13 @@ fun parsePerPackageOptions(arg: String): List<PackageOptions> { val reportUndocumented = args.find { it.endsWith("warnUndocumented") }?.startsWith("+") ?: true val privateApi = args.find { it.endsWith("privateApi") }?.startsWith("+") ?: false val suppress = args.find { it.endsWith("suppress") }?.startsWith("+") ?: false - PackageOptionsImpl(prefix, includeNonPublic = privateApi, reportUndocumented = reportUndocumented, skipDeprecated = !deprecated, suppress = suppress) + PackageOptionsImpl( + prefix, + includeNonPublic = privateApi, + reportUndocumented = reportUndocumented, + skipDeprecated = !deprecated, + suppress = suppress + ) } } @@ -52,10 +58,11 @@ class DokkaBootstrapImpl : DokkaBootstrap { override fun report() { if (warningsCount > 0 || errorsCount > 0) { - println("Generation completed with $warningsCount warning" + - (if(DokkaConsoleLogger.warningsCount == 1) "" else "s") + - " and $errorsCount error" + - if(DokkaConsoleLogger.errorsCount == 1) "" else "s" + println( + "Generation completed with $warningsCount warning" + + (if (DokkaConsoleLogger.warningsCount == 1) "" else "s") + + " and $errorsCount error" + + if (DokkaConsoleLogger.errorsCount == 1) "" else "s" ) } else { println("generation completed successfully") @@ -83,10 +90,11 @@ class DokkaBootstrapImpl : DokkaBootstrap { } val configurationWithLinks = - configuration.copy(passesConfigurations = - passesConfigurations - .map { - val links: List<ExternalDocumentationLinkImpl> = it.externalDocumentationLinks + defaultLinks(it) + configuration.copy( + passesConfigurations = + passesConfigurations.map { + val links: List<ExternalDocumentationLinkImpl> = + it.externalDocumentationLinks + defaultLinks(it) it.copy(externalDocumentationLinks = links) } ) @@ -94,8 +102,10 @@ class DokkaBootstrapImpl : DokkaBootstrap { generator = DokkaGenerator(configurationWithLinks, logger) } - override fun configure(logger: BiConsumer<String, String>, serializedConfigurationJSON: String) - = configure(DokkaProxyLogger(logger), gson.fromJson(serializedConfigurationJSON, DokkaConfigurationImpl::class.java)) + override fun configure(logger: BiConsumer<String, String>, serializedConfigurationJSON: String) = configure( + DokkaProxyLogger(logger), + gson.fromJson(serializedConfigurationJSON, DokkaConfigurationImpl::class.java) + ) override fun generate() = generator.generate() } diff --git a/core/src/main/kotlin/defaultConfiguration.kt b/core/src/main/kotlin/defaultConfiguration.kt index 148bf830..b0c12015 100644 --- a/core/src/main/kotlin/defaultConfiguration.kt +++ b/core/src/main/kotlin/defaultConfiguration.kt @@ -10,7 +10,7 @@ data class DokkaConfigurationImpl( override val cacheRoot: String?, override val impliedPlatforms: List<String>, override val passesConfigurations: List<PassConfigurationImpl>, - override val pluginsClasspath: List<File> + override var pluginsClasspath: List<File> ) : DokkaConfiguration data class PassConfigurationImpl ( 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<CommentsToContentConverter>() val signatureProvider by extensionPoint<SignatureProvider>() val locationProviderFactory by extensionPoint<LocationProviderFactory>() + val externalLocationProviderFactory by extensionPoint<ExternalLocationProviderFactory>() val outputWriter by extensionPoint<OutputWriter>() val htmlPreprocessors by extensionPoint<PageTransformer>() @@ -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<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/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<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/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<ExternalLocationProvider> = 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<DokkaBase>().query { externalLocationProviderFactory } + + 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 + 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<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] + + private val cache: MutableMap<URL, LocationInfo> = mutableMapOf() + + private fun getLocation( + dri: DRI, + jdkToExternalDocumentationLinks: Map<Int, List<DokkaConfiguration.ExternalDocumentationLink>> + ): String { + val toResolve: MutableMap<Int, MutableList<DokkaConfiguration.ExternalDocumentationLink>> = 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<String>, + val locations: Map<String, String> + ) +} + +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/DefaultLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt index c649e22b..57f53ba6 100644 --- a/plugins/base/src/main/kotlin/resolvers/DefaultLocationProviderFactory.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt @@ -1,9 +1,10 @@ -package org.jetbrains.dokka.base.resolvers +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) + override fun getLocationProvider(pageNode: RootPageNode) = + DefaultLocationProvider(pageNode, context) }
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt index 13f4563c..0814cb3e 100644 --- a/plugins/base/src/main/kotlin/resolvers/LocationProvider.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt @@ -1,7 +1,6 @@ -package org.jetbrains.dokka.base.resolvers +package org.jetbrains.dokka.base.resolvers.local 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 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<DModule>, 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<ContentNode>()) + 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 { diff --git a/plugins/gfm/build.gradle.kts b/plugins/gfm/build.gradle.kts new file mode 100644 index 00000000..c327b96c --- /dev/null +++ b/plugins/gfm/build.gradle.kts @@ -0,0 +1,8 @@ +publishing { + publications { + register<MavenPublication>("gfm-plugin") { + artifactId = "gfm-plugin" + from(components["java"]) + } + } +}
\ No newline at end of file diff --git a/plugins/gfm/src/main/kotlin/GfmPlugin.kt b/plugins/gfm/src/main/kotlin/GfmPlugin.kt new file mode 100644 index 00000000..64a2cdfc --- /dev/null +++ b/plugins/gfm/src/main/kotlin/GfmPlugin.kt @@ -0,0 +1,196 @@ +package org.jetbrains.dokka.commonmarkrenderer + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPlugin +import java.lang.StringBuilder + + +class CommonmarkRendererPlugin : DokkaPlugin() { + + val locationProviderFactory by extensionPoint<LocationProviderFactory>() + val outputWriter by extensionPoint<OutputWriter>() + + val renderer by extending { + CoreExtensions.renderer providing { CommonmarkRenderer(it.single(outputWriter), it) } + } + + val locationProvider by extending { + locationProviderFactory providing { MarkdownLocationProviderFactory(it) } order { + before(renderer) + } + } +} + +class CommonmarkRenderer( + outputWriter: OutputWriter, + context: DokkaContext +) : DefaultRenderer<StringBuilder>(outputWriter, context) { + override fun StringBuilder.buildHeader(level: Int, content: StringBuilder.() -> Unit) { + buildParagraph() + append("#".repeat(level) + " ") + content() + buildNewLine() + } + + override fun StringBuilder.buildLink(address: String, content: StringBuilder.() -> Unit) { + append("[") + content() + append("]($address)") + } + + override fun StringBuilder.buildList(node: ContentList, pageContext: ContentPage, platformRestriction: PlatformData?) { + buildParagraph() + buildListLevel(node, pageContext) + buildParagraph() + } + + private val indent = " ".repeat(4) + + private fun StringBuilder.buildListItem(items: List<ContentNode>, pageContext: ContentPage, bullet: String = "*") { + items.forEach { + if (it is ContentList) { + val builder = StringBuilder() + builder.append(indent) + builder.buildListLevel(it, pageContext) + append(builder.toString().replace(Regex(" \n(?!$)"), " \n$indent")) + } else { + append("$bullet ") + it.build(this, pageContext) + buildNewLine() + } + } + } + + private fun StringBuilder.buildListLevel(node: ContentList, pageContext: ContentPage) { + if (node.ordered) { + buildListItem( + node.children, + pageContext, + "${node.extra.allOfType<SimpleAttr>().find { it.extraKey == "start" }?.extraValue + ?: 1.also { context.logger.error("No starting number specified for ordered list in node ${pageContext.dri.first()}!")}}." + ) + } else { + buildListItem(node.children, pageContext, "*") + } + } + + override fun StringBuilder.buildNewLine() { + append(" \n") + } + + private fun StringBuilder.buildParagraph() { + append("\n\n") + } + + override fun StringBuilder.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) { + append("Resource") + } + + override fun StringBuilder.buildTable(node: ContentTable, pageContext: ContentPage, platformRestriction: PlatformData?) { + + val size = node.children.firstOrNull()?.children?.size ?: 0 + buildParagraph() + + if (node.header.size > 0) { + node.header.forEach { + it.children.forEach { + append("| ") + it.build(this, pageContext) + } + append("|\n") + } + } else { + append("| ".repeat(size)) + if (size > 0) append("|\n") + } + + append("|---".repeat(size)) + if (size > 0) append("|\n") + + node.children.forEach { + it.children.forEach { + append("| ") + it.build(this, pageContext) + } + append("|\n") + } + } + + override fun StringBuilder.buildText(textNode: ContentText) { + val decorators = decorators(textNode.style) + append(decorators) + append(textNode.text.escapeIllegalCharacters()) + append(decorators.reversed()) + } + + override fun StringBuilder.buildNavigation(page: PageNode) { + locationProvider.ancestors(page).asReversed().forEach { node -> + append("/") + if (node.isNavigable) buildLink(node, page) + else append(node.name) + } + buildParagraph() + } + + override fun buildPage(page: ContentPage, content: (StringBuilder, ContentPage) -> Unit): String = + StringBuilder().apply { + content(this, page) + }.toString() + + override fun buildError(node: ContentNode) { + context.logger.warn("Markdown renderer has encountered problem. The unmatched node is $node") + } + + private fun decorators(styles: Set<Style>) = StringBuilder().apply { + styles.forEach { + when (it) { + TextStyle.Bold -> append("**") + TextStyle.Italic -> append("*") + TextStyle.Strong -> append("**") + TextStyle.Strikethrough -> append("~~") + else -> Unit + } + } + }.toString() + + private val PageNode.isNavigable: Boolean + get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing + + private fun StringBuilder.buildLink(to: PageNode, from: PageNode) = + buildLink(locationProvider.resolve(to, from)) { + append(to.name) + } + + override fun renderPage(page: PageNode) { + val path by lazy { locationProvider.resolve(page, skipExtension = true) } + when (page) { + is ContentPage -> outputWriter.write(path, buildPage(page) { c, p -> buildPageContent(c, p) }, ".md") + is RendererSpecificPage -> when (val strategy = page.strategy) { + is RenderingStrategy.Copy -> outputWriter.writeResources(strategy.from, path) + is RenderingStrategy.Write -> outputWriter.write(path, strategy.text, "") + is RenderingStrategy.Callback -> outputWriter.write(path, strategy.instructions(this, page), ".md") + RenderingStrategy.DoNothing -> Unit + } + else -> throw AssertionError( + "Page ${page.name} cannot be rendered by renderer as it is not renderer specific nor contains content" + ) + } + } +} + +class MarkdownLocationProviderFactory(val context: DokkaContext) : LocationProviderFactory { + + override fun getLocationProvider(pageNode: RootPageNode) = MarkdownLocationProvider(pageNode, context) +} + +class MarkdownLocationProvider( + pageGraphRoot: RootPageNode, + dokkaContext: DokkaContext +) : DefaultLocationProvider( + pageGraphRoot, + dokkaContext +) { + override val extension = ".md" +}
\ No newline at end of file diff --git a/plugins/gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..ae291d68 --- /dev/null +++ b/plugins/gfm/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.gfm.GfmPlugin diff --git a/plugins/jekyll/build.gradle.kts b/plugins/jekyll/build.gradle.kts new file mode 100644 index 00000000..535a0aef --- /dev/null +++ b/plugins/jekyll/build.gradle.kts @@ -0,0 +1,8 @@ +publishing { + publications { + register<MavenPublication>("jekyll-plugin") { + artifactId = "jekyll-plugin" + from(components["java"]) + } + } +}
\ No newline at end of file diff --git a/plugins/jekyll/src/main/kotlin/JekyllPlugin.kt b/plugins/jekyll/src/main/kotlin/JekyllPlugin.kt new file mode 100644 index 00000000..0a0a2cc4 --- /dev/null +++ b/plugins/jekyll/src/main/kotlin/JekyllPlugin.kt @@ -0,0 +1,182 @@ +package org.jetbrains.dokka.jekyll + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.plugability.single +import org.jetbrains.dokka.renderers.DefaultRenderer +import org.jetbrains.dokka.renderers.OutputWriter +import org.jetbrains.dokka.resolvers.DefaultLocationProvider +import org.jetbrains.dokka.resolvers.LocationProvider +import org.jetbrains.dokka.resolvers.LocationProviderFactory +import java.lang.StringBuilder + + +class JekyllPlugin : DokkaPlugin() { + + val renderer by extending { + CoreExtensions.renderer providing { JekyllRenderer(it.single(CoreExtensions.outputWriter), it) } + } +} + +class JekyllRenderer( + outputWriter: OutputWriter, + context: DokkaContext +) : DefaultRenderer<StringBuilder>(outputWriter, context) { + override fun StringBuilder.buildHeader(level: Int, content: StringBuilder.() -> Unit) { + buildParagraph() + append("${"#".repeat(level)} ") + content() + buildNewLine() + } + + override fun StringBuilder.buildLink(address: String, content: StringBuilder.() -> Unit) { + append("[") + content() + append("]($address)") + } + + override fun StringBuilder.buildList(node: ContentList, pageContext: ContentPage) { + buildListLevel(node, pageContext) + buildParagraph() + } + + private val indent = " ".repeat(4) + + private fun StringBuilder.buildListItem(items: List<ContentNode>, pageContext: ContentPage, bullet: String = "*") { + items.forEach { + if(it is ContentList) { + val builder = StringBuilder() + builder.append(indent) + builder.buildListLevel(it, pageContext) + append(builder.toString().replace(Regex(" \n(?!$)"), " \n$indent")) + } else { + append("$bullet ") + it.build(this, pageContext) + buildNewLine() + } + } + } + + private fun StringBuilder.buildListLevel(node: ContentList, pageContext: ContentPage) { + if(node.ordered) { + buildListItem(node.children, pageContext, "${node.start}.") + } else { + buildListItem(node.children, pageContext, "*") + } + } + + override fun StringBuilder.buildNewLine() { + this.append(" \n") + } + + fun StringBuilder.buildParagraph() { + this.append("\n\n") + } + + override fun StringBuilder.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) { + this.append("Resource") + } + + override fun StringBuilder.buildTable(node: ContentTable, pageContext: ContentPage) { + + buildParagraph() + + val size = node.children.firstOrNull()?.children?.size ?: 0 + + if(node.header.size > 0) { + node.header.forEach { + it.children.forEach { + append("| ") + it.build(this, pageContext) + } + append("|\n") + } + } else { + append("| ".repeat(size)) + if(size > 0) append("|\n") + } + + append("|---".repeat(size)) + if(size > 0) append("|\n") + + + node.children.forEach { + it.children.forEach { + append("| ") + it.build(this, pageContext) + } + append("|\n") + } + + buildParagraph() + } + + override fun StringBuilder.buildText(textNode: ContentText) { + val decorators = decorators(textNode.style) + this.append(decorators) + this.append(textNode.text.replace(Regex("[<>]"), "")) + this.append(decorators.reversed()) + } + + override fun StringBuilder.buildNavigation(page: PageNode) { + locationProvider.ancestors(page).asReversed().forEach { node -> + append("/") + if (node.isNavigable) buildLink(node, page) + else append(node.name) + } + buildParagraph() + } + + override fun buildPage(page: ContentPage, content: (StringBuilder, ContentPage) -> Unit): String { + val builder = StringBuilder() + builder.append("---\n") + builder.append("title: ${page.name} -\n") + builder.append("---\n") + content(builder, page) + return builder.toString() + } + + override fun buildError(node: ContentNode) { + println("Error") + } + + private fun decorators(styles: Set<Style>): String { + val decorators = StringBuilder() + styles.forEach { + when(it) { + TextStyle.Bold -> decorators.append("**") + TextStyle.Italic -> decorators.append("*") + TextStyle.Strong -> decorators.append("**") + TextStyle.Strikethrough -> decorators.append("~~") + else -> Unit + } + } + return decorators.toString() + } + + private val PageNode.isNavigable: Boolean + get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing + + private fun StringBuilder.buildLink(to: PageNode, from: PageNode) = + buildLink(locationProvider.resolve(to, from)) { + append(to.name) + } + + override fun renderPage(page: PageNode) { + val path by lazy { locationProvider.resolve(page, skipExtension = true) } + when (page) { + is ContentPage -> outputWriter.write(path, buildPage(page) { c, p -> buildPageContent(c, p) }, ".md") + is RendererSpecificPage -> when (val strategy = page.strategy) { + is RenderingStrategy.Copy -> outputWriter.writeResources(strategy.from, path) + is RenderingStrategy.Write -> outputWriter.write(path, strategy.text, "") + is RenderingStrategy.Callback -> outputWriter.write(path, strategy.instructions(this, page), ".md") + RenderingStrategy.DoNothing -> Unit + } + else -> throw AssertionError( + "Page ${page.name} cannot be rendered by renderer as it is not renderer specific nor contains content" + ) + } + } +}
\ No newline at end of file diff --git a/plugins/jekyll/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/plugins/jekyll/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..92c75544 --- /dev/null +++ b/plugins/jekyll/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1 @@ +org.jetbrains.dokka.jekyll.JekyllPlugin diff --git a/runners/cli/src/main/kotlin/cli/main.kt b/runners/cli/src/main/kotlin/cli/main.kt index e9329c38..644ecee4 100644 --- a/runners/cli/src/main/kotlin/cli/main.kt +++ b/runners/cli/src/main/kotlin/cli/main.kt @@ -52,7 +52,7 @@ open class GlobalArguments(parser: DokkaArgumentsParser) : DokkaConfiguration { listOf("-pass"), "Single dokka pass" ) { - Arguments(parser) + Arguments(parser).also { if(it.moduleName.isEmpty()) DokkaConsoleLogger.warn("Not specified module name. It can result in unexpected behaviour while including documentation for module") } } } @@ -260,14 +260,18 @@ object MainKt { listOf("-globalPackageOptions"), "List of package passConfiguration in format \"prefix,-deprecated,-privateApi,+warnUndocumented,+suppress;...\" " ) { link -> - configuration.passesConfigurations.all { it.perPackageOptions.addAll(parsePerPackageOptions(link)) } + configuration.passesConfigurations.all { + it.perPackageOptions.toMutableList().addAll(parsePerPackageOptions(link)) + } } parseContext.cli.singleAction( listOf("-globalLinks"), "External documentation links in format url^packageListUrl^^url2..." ) { link -> - configuration.passesConfigurations.all { it.externalDocumentationLinks.addAll(parseLinks(link)) } + configuration.passesConfigurations.all { + it.externalDocumentationLinks.toMutableList().addAll(parseLinks(link)) + } } parseContext.cli.repeatingAction( @@ -278,12 +282,14 @@ object MainKt { listOf(SourceLinkDefinitionImpl.parseSourceLinkDefinition(it)) else { if (it.isNotEmpty()) { - println("Warning: Invalid -srcLink syntax. Expected: <path>=<url>[#lineSuffix]. No source links will be generated.") + DokkaConsoleLogger.warn("Invalid -srcLink syntax. Expected: <path>=<url>[#lineSuffix]. No source links will be generated.") } listOf() } - configuration.passesConfigurations.all { it.sourceLinks.addAll(newSourceLinks) } + configuration.passesConfigurations.all { + it.sourceLinks.toMutableList().addAll(newSourceLinks) + } } parser.parseInto(configuration) diff --git a/runners/gradle-plugin/build.gradle.kts b/runners/gradle-plugin/build.gradle.kts index 0d68a525..df4a8738 100644 --- a/runners/gradle-plugin/build.gradle.kts +++ b/runners/gradle-plugin/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { compileOnly("com.android.tools.build:gradle-core:3.0.0") compileOnly("com.android.tools.build:builder-model:3.0.0") compileOnly(gradleApi()) + compileOnly(gradleKotlinDsl()) constraints { val kotlin_version: String by project compileOnly("org.jetbrains.kotlin:kotlin-reflect:${kotlin_version}") { diff --git a/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaCollectorTask.kt b/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaCollectorTask.kt new file mode 100644 index 00000000..f4fa7aaa --- /dev/null +++ b/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaCollectorTask.kt @@ -0,0 +1,56 @@ +package org.jetbrains.dokka.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.UnknownTaskException +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.getByName +import java.lang.IllegalStateException + +open class DokkaCollectorTask : DefaultTask() { + + @Input + var modules: List<String> = emptyList() + + @Input + var outputDirectory: String = "" + + private lateinit var configuration: GradleDokkaConfigurationImpl + + @TaskAction + fun collect() { + val passesConfigurations = getProjects(project).filter { it.name in modules }.map { + val task = try { + it.tasks.getByName(DOKKA_TASK_NAME, DokkaTask::class) + } catch (e: UnknownTaskException) { + throw IllegalStateException("No dokka task declared in module ${it.name}") + } + task.getConfiguration() + } + + val initial = GradleDokkaConfigurationImpl().apply { + outputDir = outputDirectory + cacheRoot = passesConfigurations.first().cacheRoot + format = passesConfigurations.first().format + generateIndexPages = passesConfigurations.first().generateIndexPages + } + + configuration = passesConfigurations.fold(initial) { acc, it: GradleDokkaConfigurationImpl -> + if(acc.format != it.format || acc.generateIndexPages != it.generateIndexPages || acc.cacheRoot != it.cacheRoot) + throw IllegalStateException("Dokka task configurations differ on core arguments (format, generateIndexPages, cacheRoot)") + acc.passesConfigurations = acc.passesConfigurations + it.passesConfigurations + acc.pluginsClasspath = (acc.pluginsClasspath + it.pluginsClasspath).distinct() + acc + } + project.tasks.getByName(DOKKA_TASK_NAME).setProperty("config", configuration) + } + + init { + finalizedBy(project.tasks.getByName(DOKKA_TASK_NAME)) + } + + private fun getProjects(project: Project): Set<Project> = + project.subprojects + project.subprojects.flatMap { getProjects(it) } + +}
\ No newline at end of file diff --git a/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaTask.kt b/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaTask.kt index 65b0f4b3..4f7b88c4 100644 --- a/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaTask.kt +++ b/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaTask.kt @@ -8,7 +8,6 @@ import org.gradle.api.internal.plugins.DslObject import org.gradle.api.plugins.JavaBasePlugin import org.gradle.api.tasks.* import org.jetbrains.dokka.DokkaBootstrap -import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink.Builder import org.jetbrains.dokka.DokkaConfiguration.SourceRoot import org.jetbrains.dokka.Platform @@ -76,10 +75,11 @@ open class DokkaTask : DefaultTask() { @Nested get() = DslObject(this).extensions.getByType(GradlePassConfigurationImpl::class.java) internal set(value) = DslObject(this).extensions.add(CONFIGURATION_EXTENSION_NAME, value) + var config: GradleDokkaConfigurationImpl? = null + // Configure Dokka with closure in Gradle Kotlin DSL fun configuration(action: Action<in GradlePassConfigurationImpl>) = action.execute(configuration) - private val kotlinTasks: List<Task> by lazy { extractKotlinCompileTasks(configuration.collectKotlinTasks ?: { defaultKotlinTasks() }) } private val configExtractor = ConfigurationExtractor(project) @@ -133,6 +133,10 @@ open class DokkaTask : DefaultTask() { @TaskAction fun generate() { + generateForConfig(config ?: getConfiguration()) + } + + internal fun generateForConfig(configuration: GradleDokkaConfigurationImpl) { outputDiagnosticInfo = true val kotlinColorsEnabledBefore = System.getProperty(COLORS_ENABLED_PROPERTY) ?: "false" System.setProperty(COLORS_ENABLED_PROPERTY, "false") @@ -146,20 +150,6 @@ open class DokkaTask : DefaultTask() { val gson = GsonBuilder().setPrettyPrinting().create() - val globalConfig = multiplatform.toList().find { it.name.toLowerCase() == GLOBAL_PLATFORM_NAME } - val passConfigurationList = collectConfigurations() - .map { defaultPassConfiguration(it, globalConfig) } - - val configuration = GradleDokkaConfigurationImpl().apply { - outputDir = outputDirectory - format = outputFormat - generateIndexPages = true - cacheRoot = cacheRoot - impliedPlatforms = impliedPlatforms - passesConfigurations = passConfigurationList - pluginsClasspath = pluginsConfiguration.resolve().toList() - } - bootstrapProxy.configure( BiConsumer { level, message -> when (level) { @@ -180,6 +170,21 @@ open class DokkaTask : DefaultTask() { } } + internal fun getConfiguration(): GradleDokkaConfigurationImpl { + val globalConfig = multiplatform.toList().find { it.name.toLowerCase() == GLOBAL_PLATFORM_NAME } + val defaultModulesConfiguration = collectConfigurations() + .map { defaultPassConfiguration(it, globalConfig) } + return GradleDokkaConfigurationImpl().apply { + outputDir = outputDirectory + format = outputFormat + generateIndexPages = true + cacheRoot = cacheRoot + impliedPlatforms = impliedPlatforms + passesConfigurations = defaultModulesConfiguration + pluginsClasspath = pluginsConfiguration.resolve().toList() + } + } + private fun collectConfigurations() = if (this.isMultiplatformProject()) collectMultiplatform() else listOf(collectSinglePlatform(configuration)) diff --git a/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/main.kt b/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/main.kt index f4ffdab6..4efc5010 100644 --- a/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/main.kt +++ b/runners/gradle-plugin/src/main/kotlin/org/jetbrains/dokka/gradle/main.kt @@ -10,9 +10,10 @@ import java.util.* internal const val CONFIGURATION_EXTENSION_NAME = "configuration" internal const val MULTIPLATFORM_EXTENSION_NAME = "multiplatform" +internal const val DOKKA_TASK_NAME = "dokka" +internal const val DOKKA_COLLECTOR_TASK_NAME = "dokkaCollector" open class DokkaPlugin : Plugin<Project> { - private val taskName = "dokka" override fun apply(project: Project) { loadDokkaVersion() @@ -20,33 +21,50 @@ open class DokkaPlugin : Plugin<Project> { val pluginsConfiguration = project.configurations.create("dokkaPlugins").apply { defaultDependencies { it.add(project.dependencies.create("org.jetbrains.dokka:dokka-base:${DokkaVersion.version}")) } } - addTasks(project, dokkaRuntimeConfiguration, pluginsConfiguration, DokkaTask::class.java) + addDokkaTasks(project, dokkaRuntimeConfiguration, pluginsConfiguration, DokkaTask::class.java) + addDokkaCollectorTasks(project, DokkaCollectorTask::class.java) } - private fun loadDokkaVersion() = DokkaVersion.loadFrom(javaClass.getResourceAsStream("/META-INF/gradle-plugins/org.jetbrains.dokka.properties")) + private fun loadDokkaVersion() = + DokkaVersion.loadFrom(javaClass.getResourceAsStream("/META-INF/gradle-plugins/org.jetbrains.dokka.properties")) private fun addConfiguration(project: Project) = project.configurations.create("dokkaRuntime").apply { - defaultDependencies{ dependencies -> dependencies.add(project.dependencies.create("org.jetbrains.dokka:dokka-core:${DokkaVersion.version}")) } + defaultDependencies { dependencies -> dependencies.add(project.dependencies.create("org.jetbrains.dokka:dokka-core:${DokkaVersion.version}")) } } - private fun addTasks( + private fun addDokkaTasks( project: Project, runtimeConfiguration: Configuration, pluginsConfiguration: Configuration, taskClass: Class<out DokkaTask> ) { - if(GradleVersion.current() >= GradleVersion.version("4.10")) { - project.tasks.register(taskName, taskClass) + if (GradleVersion.current() >= GradleVersion.version("4.10")) { + project.tasks.register(DOKKA_TASK_NAME, taskClass) } else { - project.tasks.create(taskName, taskClass) + project.tasks.create(DOKKA_TASK_NAME, taskClass) } project.tasks.withType(taskClass) { task -> task.multiplatform = project.container(GradlePassConfigurationImpl::class.java) task.configuration = GradlePassConfigurationImpl() task.dokkaRuntime = runtimeConfiguration task.pluginsConfiguration = pluginsConfiguration - task.outputDirectory = File(project.buildDir, taskName).absolutePath + task.outputDirectory = File(project.buildDir, DOKKA_TASK_NAME).absolutePath + } + } + + private fun addDokkaCollectorTasks( + project: Project, + taskClass: Class<out DokkaCollectorTask> + ) { + if (GradleVersion.current() >= GradleVersion.version("4.10")) { + project.tasks.register(DOKKA_COLLECTOR_TASK_NAME, taskClass) + } else { + project.tasks.create(DOKKA_COLLECTOR_TASK_NAME, taskClass) + } + project.tasks.withType(taskClass) { task -> + task.modules = emptyList() + task.outputDirectory = File(project.buildDir, DOKKA_TASK_NAME).absolutePath } } } diff --git a/runners/maven-plugin/src/main/kotlin/DokkaMojo.kt b/runners/maven-plugin/src/main/kotlin/DokkaMojo.kt index c84b97f4..cfe278ce 100644 --- a/runners/maven-plugin/src/main/kotlin/DokkaMojo.kt +++ b/runners/maven-plugin/src/main/kotlin/DokkaMojo.kt @@ -29,6 +29,7 @@ import org.eclipse.aether.transport.file.FileTransporterFactory import org.eclipse.aether.transport.http.HttpTransporterFactory import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator import org.jetbrains.dokka.* +import org.jetbrains.dokka.utilities.DokkaConsoleLogger import java.io.File import java.net.URL @@ -219,18 +220,22 @@ abstract class AbstractDokkaMojo : AbstractMojo() { includeRootPackage = includeRootPackage ) + val logger = MavenDokkaLogger(log) + val configuration = DokkaConfigurationImpl( outputDir = getOutDir(), format = getOutFormat(), impliedPlatforms = impliedPlatforms, cacheRoot = cacheRoot, - passesConfigurations = listOf(passConfiguration), + passesConfigurations = listOf(passConfiguration).also { + if(passConfiguration.moduleName.isEmpty()) logger.warn("Not specified module name. It can result in unexpected behaviour while including documentation for module") + }, generateIndexPages = generateIndexPages, pluginsClasspath = getArtifactByAether("org.jetbrains.dokka", "dokka-base", dokkaVersion) + dokkaPlugins.map { getArtifactByAether(it.groupId, it.artifactId, it.version) }.flatten() ) - val gen = DokkaGenerator(configuration, MavenDokkaLogger(log)) + val gen = DokkaGenerator(configuration, logger) gen.generate() } diff --git a/testApi/src/main/kotlin/testApi/testRunner/TestRunner.kt b/testApi/src/main/kotlin/testApi/testRunner/TestRunner.kt index 64d16fef..d2db90b7 100644 --- a/testApi/src/main/kotlin/testApi/testRunner/TestRunner.kt +++ b/testApi/src/main/kotlin/testApi/testRunner/TestRunner.kt @@ -63,8 +63,9 @@ abstract class AbstractCoreTest { val newConfiguration = configuration.copy( outputDir = testDirPath.toAbsolutePath().toString(), - passesConfigurations = configuration.passesConfigurations - .map { it.copy(sourceRoots = it.sourceRoots.map { it.copy(path = "${testDirPath.toAbsolutePath()}/${it.path}") }) } + passesConfigurations = configuration.passesConfigurations.map { + it.copy(sourceRoots = it.sourceRoots.map { it.copy(path = "${testDirPath.toAbsolutePath()}/${it.path}") }) + } ) DokkaTestGenerator( newConfiguration, @@ -139,7 +140,6 @@ abstract class AbstractCoreTest { var cacheRoot: String? = null var pluginsClasspath: List<File> = emptyList() private val passesConfigurations = mutableListOf<PassConfigurationImpl>() - fun build() = DokkaConfigurationImpl( outputDir = outputDir, format = format, |