diff options
Diffstat (limited to 'dokka-subprojects/plugin-all-modules-page')
15 files changed, 945 insertions, 0 deletions
diff --git a/dokka-subprojects/plugin-all-modules-page/README.md b/dokka-subprojects/plugin-all-modules-page/README.md new file mode 100644 index 00000000..459c3ef5 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/README.md @@ -0,0 +1,7 @@ +# All Modules plugin + +The All Modules plugin is used for documenting multi-module projects. It creates a common table of contents +for all submodules and helps resolve cross-module links and resource locations. + +You can find the All Modules plugin on +[Maven Central](https://mvnrepository.com/artifact/org.jetbrains.dokka/all-modules-page-plugin). diff --git a/dokka-subprojects/plugin-all-modules-page/api/plugin-all-modules-page.api b/dokka-subprojects/plugin-all-modules-page/api/plugin-all-modules-page.api new file mode 100644 index 00000000..3e6dc898 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/api/plugin-all-modules-page.api @@ -0,0 +1,87 @@ +public final class org/jetbrains/dokka/allModulesPage/AllModulesPageGeneration : org/jetbrains/dokka/generation/Generation { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public final fun createAllModulesPage (Lorg/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext;)Lorg/jetbrains/dokka/pages/RootPageNode; + public final fun finishProcessingSubmodules ()V + public fun generate (Lorg/jetbrains/dokka/Timer;)V + public fun getGenerationName ()Ljava/lang/String; + public final fun processMultiModule (Lorg/jetbrains/dokka/pages/RootPageNode;)V + public final fun processSubmodules ()Lorg/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext; + public final fun render (Lorg/jetbrains/dokka/pages/RootPageNode;)V + public final fun runPostActions ()V + public final fun transformAllModulesPage (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/pages/RootPageNode; +} + +public final class org/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext : org/jetbrains/dokka/transformers/pages/CreationContext { + public fun <init> (Ljava/util/List;)V + public fun <init> (Lorg/jetbrains/dokka/templates/TemplatingResult;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lorg/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext;Ljava/util/List;ILjava/lang/Object;)Lorg/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext; + public fun equals (Ljava/lang/Object;)Z + public final fun getNonEmptyModules ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/allModulesPage/AllModulesPagePlugin : org/jetbrains/dokka/plugability/DokkaPlugin { + public fun <init> ()V + public final fun getAllModulesPageCreator ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getAllModulesPageCreators ()Lorg/jetbrains/dokka/plugability/Extension; + public final fun getAllModulesPageGeneration ()Lorg/jetbrains/dokka/plugability/Extension; + public final fun getAllModulesPageTransformer ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getBaseLocationProviderFactory ()Lorg/jetbrains/dokka/plugability/Extension; + public final fun getExternalModuleLinkResolver ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getMultiModuleLinkResolver ()Lorg/jetbrains/dokka/plugability/Extension; + public final fun getMultimoduleLocationProvider ()Lorg/jetbrains/dokka/plugability/Extension; + public final fun getPartialLocationProviderFactory ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; + public final fun getResolveLinkCommandHandler ()Lorg/jetbrains/dokka/plugability/Extension; +} + +public final class org/jetbrains/dokka/allModulesPage/DefaultExternalModuleLinkResolver : org/jetbrains/dokka/allModulesPage/ExternalModuleLinkResolver { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; + public fun resolve (Lorg/jetbrains/dokka/links/DRI;Ljava/io/File;)Ljava/lang/String; + public fun resolveLinkToModuleIndex (Ljava/lang/String;)Ljava/lang/String; +} + +public abstract interface class org/jetbrains/dokka/allModulesPage/ExternalModuleLinkResolver { + public abstract fun resolve (Lorg/jetbrains/dokka/links/DRI;Ljava/io/File;)Ljava/lang/String; + public abstract fun resolveLinkToModuleIndex (Ljava/lang/String;)Ljava/lang/String; +} + +public class org/jetbrains/dokka/allModulesPage/MultimoduleLocationProvider : org/jetbrains/dokka/base/resolvers/local/DokkaBaseLocationProvider { + public fun <init> (Lorg/jetbrains/dokka/pages/RootPageNode;Lorg/jetbrains/dokka/plugability/DokkaContext;Ljava/lang/String;)V + public synthetic fun <init> (Lorg/jetbrains/dokka/pages/RootPageNode;Lorg/jetbrains/dokka/plugability/DokkaContext;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun ancestors (Lorg/jetbrains/dokka/pages/PageNode;)Ljava/util/List; + public final fun getExtension ()Ljava/lang/String; + public fun pathToRoot (Lorg/jetbrains/dokka/pages/PageNode;)Ljava/lang/String; + public fun resolve (Lorg/jetbrains/dokka/links/DRI;Ljava/util/Set;Lorg/jetbrains/dokka/pages/PageNode;)Ljava/lang/String; + public fun resolve (Lorg/jetbrains/dokka/pages/PageNode;Lorg/jetbrains/dokka/pages/PageNode;Z)Ljava/lang/String; +} + +public final class org/jetbrains/dokka/allModulesPage/MultimoduleLocationProvider$Factory : org/jetbrains/dokka/base/resolvers/local/LocationProviderFactory { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun getLocationProvider (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/base/resolvers/local/LocationProvider; +} + +public final class org/jetbrains/dokka/allModulesPage/MultimodulePageCreator : org/jetbrains/dokka/transformers/pages/PageCreator { + public static final field Companion Lorg/jetbrains/dokka/allModulesPage/MultimodulePageCreator$Companion; + public static final field MULTIMODULE_PACKAGE_PLACEHOLDER Ljava/lang/String; + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun invoke (Lorg/jetbrains/dokka/allModulesPage/AllModulesPageGeneration$DefaultAllModulesContext;)Lorg/jetbrains/dokka/pages/RootPageNode; + public synthetic fun invoke (Lorg/jetbrains/dokka/transformers/pages/CreationContext;)Lorg/jetbrains/dokka/pages/RootPageNode; +} + +public final class org/jetbrains/dokka/allModulesPage/MultimodulePageCreator$Companion { + public final fun getMULTIMODULE_ROOT_DRI ()Lorg/jetbrains/dokka/links/DRI; +} + +public final class org/jetbrains/dokka/allModulesPage/ResolveLinkCommandHandler : org/jetbrains/dokka/templates/CommandHandler { + public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun canHandle (Lorg/jetbrains/dokka/base/templating/Command;)Z + public fun finish (Ljava/io/File;)V + public fun handleCommand (Lorg/jsoup/nodes/Element;Lorg/jetbrains/dokka/base/templating/Command;Ljava/io/File;Ljava/io/File;)V + public fun handleCommandAsComment (Lorg/jetbrains/dokka/base/templating/Command;Ljava/util/List;Ljava/io/File;Ljava/io/File;)V + public fun handleCommandAsTag (Lorg/jetbrains/dokka/base/templating/Command;Lorg/jsoup/nodes/Element;Ljava/io/File;Ljava/io/File;)V +} + diff --git a/dokka-subprojects/plugin-all-modules-page/build.gradle.kts b/dokka-subprojects/plugin-all-modules-page/build.gradle.kts new file mode 100644 index 00000000..a031684d --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import dokkabuild.overridePublicationArtifactId + +plugins { + id("dokkabuild.kotlin-jvm") + id("dokkabuild.publish-jvm") +} + +overridePublicationArtifactId("all-modules-page-plugin") + +dependencies { + compileOnly(projects.dokkaSubprojects.dokkaCore) + compileOnly(projects.dokkaSubprojects.analysisKotlinApi) + + implementation(projects.dokkaSubprojects.pluginBase) + implementation(projects.dokkaSubprojects.pluginTemplating) + + implementation(projects.dokkaSubprojects.analysisMarkdownJb) + + implementation(libs.kotlinx.html) + + testImplementation(kotlin("test")) + testImplementation(projects.dokkaSubprojects.pluginBase) + testImplementation(projects.dokkaSubprojects.pluginBaseTestUtils) + testImplementation(projects.dokkaSubprojects.pluginGfm) + testImplementation(projects.dokkaSubprojects.pluginGfmTemplateProcessing) + testImplementation(projects.dokkaSubprojects.coreContentMatcherTestUtils) + testImplementation(projects.dokkaSubprojects.coreTestApi) +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPageGeneration.kt b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPageGeneration.kt new file mode 100644 index 00000000..11d2d32c --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPageGeneration.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.Timer +import org.jetbrains.dokka.generation.Generation +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.templates.TemplatingPlugin +import org.jetbrains.dokka.templates.TemplatingResult +import org.jetbrains.dokka.transformers.pages.CreationContext + +public class AllModulesPageGeneration(private val context: DokkaContext) : Generation { + + private val allModulesPagePlugin by lazy { context.plugin<AllModulesPagePlugin>() } + private val templatingPlugin by lazy { context.plugin<TemplatingPlugin>() } + + override fun Timer.generate() { + report("Processing submodules") + val generationContext = processSubmodules() + + report("Creating all modules page") + val pages = createAllModulesPage(generationContext) + + report("Transforming pages") + val transformedPages = transformAllModulesPage(pages) + + report("Rendering") + render(transformedPages) + + report("Processing multimodule") + processMultiModule(transformedPages) + + report("Finish submodule processing") + finishProcessingSubmodules() + + report("Running post-actions") + runPostActions() + } + + override val generationName: String = "index page for project" + + public fun createAllModulesPage(allModulesContext: DefaultAllModulesContext): RootPageNode = + allModulesPagePlugin.querySingle { allModulesPageCreator }.invoke(allModulesContext) + + public fun transformAllModulesPage(pages: RootPageNode): RootPageNode = + allModulesPagePlugin.query { allModulesPageTransformer }.fold(pages) { acc, t -> t(acc) } + + public fun render(transformedPages: RootPageNode) { + context.single(CoreExtensions.renderer).render(transformedPages) + } + + public fun runPostActions() { + context[CoreExtensions.postActions].forEach { it() } + } + + public fun processSubmodules(): DefaultAllModulesContext { + return templatingPlugin.querySingle { submoduleTemplateProcessor } + .process(context.configuration.modules) + .let { DefaultAllModulesContext(it) } + } + + public fun processMultiModule(root: RootPageNode) { + templatingPlugin.querySingle { multimoduleTemplateProcessor }.process(root) + } + + public fun finishProcessingSubmodules() { + templatingPlugin.query { templateProcessingStrategy }.forEach { it.finish(context.configuration.outputDir) } + } + + public data class DefaultAllModulesContext(val nonEmptyModules: List<String>) : CreationContext { + public constructor(templating: TemplatingResult) : this(templating.modules) + } +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPagePlugin.kt b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPagePlugin.kt new file mode 100644 index 00000000..06202082 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPagePlugin.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory +import org.jetbrains.dokka.generation.Generation +import org.jetbrains.dokka.plugability.* +import org.jetbrains.dokka.templates.CommandHandler +import org.jetbrains.dokka.templates.TemplatingPlugin +import org.jetbrains.dokka.transformers.pages.PageCreator +import org.jetbrains.dokka.transformers.pages.PageTransformer + +public class AllModulesPagePlugin : DokkaPlugin() { + + public val partialLocationProviderFactory: ExtensionPoint<LocationProviderFactory> by extensionPoint() + public val allModulesPageCreator: ExtensionPoint<PageCreator<AllModulesPageGeneration.DefaultAllModulesContext>> by extensionPoint() + public val allModulesPageTransformer: ExtensionPoint<PageTransformer> by extensionPoint() + public val externalModuleLinkResolver: ExtensionPoint<ExternalModuleLinkResolver> by extensionPoint() + + public val allModulesPageCreators: Extension<PageCreator<AllModulesPageGeneration.DefaultAllModulesContext>, *, *> by extending { + allModulesPageCreator providing ::MultimodulePageCreator + } + + private val dokkaBase: DokkaBase by lazy { plugin<DokkaBase>() } + + public val multimoduleLocationProvider: Extension<LocationProviderFactory, *, *> by extending { + (dokkaBase.locationProviderFactory + providing MultimoduleLocationProvider::Factory + override plugin<DokkaBase>().locationProvider) + } + + public val baseLocationProviderFactory: Extension<LocationProviderFactory, *, *> by extending { + partialLocationProviderFactory providing ::DokkaLocationProviderFactory + } + + public val allModulesPageGeneration: Extension<Generation, *, *> by extending { + (CoreExtensions.generation + providing ::AllModulesPageGeneration + override dokkaBase.singleGeneration) + } + + public val resolveLinkCommandHandler: Extension<CommandHandler, *, *> by extending { + plugin<TemplatingPlugin>().directiveBasedCommandHandlers providing ::ResolveLinkCommandHandler + } + + public val multiModuleLinkResolver: Extension<ExternalModuleLinkResolver, *, *> by extending { + externalModuleLinkResolver providing ::DefaultExternalModuleLinkResolver + } + + @OptIn(DokkaPluginApiPreview::class) + override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = + PluginApiPreviewAcknowledgement +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ExternalModuleLinkResolver.kt b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ExternalModuleLinkResolver.kt new file mode 100644 index 00000000..da747bda --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ExternalModuleLinkResolver.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.DokkaConfiguration.DokkaModuleDescription +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentation +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.resolvers.shared.PackageList.Companion.PACKAGE_LIST_NAME +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import java.io.File +import java.net.URL + +public interface ExternalModuleLinkResolver { + public fun resolve(dri: DRI, fileContext: File): String? + public fun resolveLinkToModuleIndex(moduleName: String): String? +} + +public class DefaultExternalModuleLinkResolver( + public val context: DokkaContext +) : ExternalModuleLinkResolver { + private val elpFactory = context.plugin<DokkaBase>().query { externalLocationProviderFactory } + private val externalDocumentations by lazy(::setupExternalDocumentations) + private val elps by lazy { + elpFactory.flatMap { externalDocumentations.map { ed -> it.getExternalLocationProvider(ed) } }.filterNotNull() + } + + private fun setupExternalDocumentations(): List<ExternalDocumentation> = + context.configuration.modules.mapNotNull { module -> + loadPackageListForModule(module)?.let { packageList -> + ExternalDocumentation( + URL("file:/${module.relativePathToOutputDirectory.toRelativeOutputDir()}"), + packageList + ) + } + } + + + private fun File.toRelativeOutputDir(): File = if (isAbsolute) { + relativeToOrSelf(context.configuration.outputDir) + } else { + this + } + + private fun loadPackageListForModule(module: DokkaModuleDescription) = + module.sourceOutputDirectory.walkTopDown().maxDepth(3).firstOrNull { it.name == PACKAGE_LIST_NAME }?.let { + PackageList.load( + URL("file:" + it.path), + 8, + true + ) + } + + override fun resolve(dri: DRI, fileContext: File): String? { + val absoluteLink = elps.mapNotNull { it.resolve(dri) }.firstOrNull() ?: return null + val modulePath = context.configuration.outputDir.absolutePath.split(File.separator) + val contextPath = fileContext.absolutePath.split(File.separator) + val commonPathElements = modulePath.zip(contextPath) + .takeWhile { (a, b) -> a == b }.count() + + return (List(contextPath.size - commonPathElements - 1) { ".." } + modulePath.drop(commonPathElements)).joinToString( + "/" + ) + absoluteLink.removePrefix("file:") + } + + override fun resolveLinkToModuleIndex(moduleName: String): String? = + context.configuration.modules.firstOrNull { it.name == moduleName } + ?.let { module -> + val packageList = loadPackageListForModule(module) + val extension = packageList?.linkFormat?.linkExtension?.let { ".$it" }.orEmpty() + "${module.relativePathToOutputDirectory}/index$extension" + } + +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimoduleLocationProvider.kt b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimoduleLocationProvider.kt new file mode 100644 index 00000000..b0fa13d0 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimoduleLocationProvider.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.allModulesPage.MultimodulePageCreator.Companion.MULTIMODULE_PACKAGE_PLACEHOLDER +import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider +import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DisplaySourceSet +import org.jetbrains.dokka.pages.ContentPage +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle + +public open class MultimoduleLocationProvider( + private val root: RootPageNode, dokkaContext: DokkaContext, + public val extension: String = ".html" +) : DokkaBaseLocationProvider(root, dokkaContext) { + + private val defaultLocationProvider = + dokkaContext.plugin<AllModulesPagePlugin>().querySingle { partialLocationProviderFactory } + .getLocationProvider(root) + private val externalModuleLinkResolver = + dokkaContext.plugin<AllModulesPagePlugin>().querySingle { externalModuleLinkResolver } + + override fun resolve(dri: DRI, sourceSets: Set<DisplaySourceSet>, context: PageNode?): String? { + return if (dri == MultimodulePageCreator.MULTIMODULE_ROOT_DRI) { + pathToRoot(root) + "index" + } else { + dri.takeIf { it.packageName == MULTIMODULE_PACKAGE_PLACEHOLDER } + ?.classNames + ?.let(externalModuleLinkResolver::resolveLinkToModuleIndex) + } + } + + override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean): String? { + return if (node is ContentPage && MultimodulePageCreator.MULTIMODULE_ROOT_DRI in node.dri) { + pathToRoot(root) + "index" + if (!skipExtension) extension else "" + } else { + defaultLocationProvider.resolve(node, context, skipExtension) + } + } + + override fun pathToRoot(from: PageNode): String = defaultLocationProvider.pathToRoot(from) + + override fun ancestors(node: PageNode): List<PageNode> = listOf(root) + + public class Factory( + private val context: DokkaContext + ) : LocationProviderFactory { + override fun getLocationProvider(pageNode: RootPageNode): LocationProvider = + MultimoduleLocationProvider(pageNode, context) + } +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimodulePageCreator.kt b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimodulePageCreator.kt new file mode 100644 index 00000000..7b832d21 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimodulePageCreator.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.DokkaConfiguration.DokkaModuleDescription +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.P +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.pages.PageCreator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import java.io.File + +public class MultimodulePageCreator( + private val context: DokkaContext, +) : PageCreator<AllModulesPageGeneration.DefaultAllModulesContext> { + private val commentsConverter by lazy { context.plugin<DokkaBase>().querySingle { commentsToContentConverter } } + private val signatureProvider by lazy { context.plugin<DokkaBase>().querySingle { signatureProvider } } + private val moduleDocumentationReader by lazy { context.plugin<InternalKotlinAnalysisPlugin>().querySingle { moduleAndPackageDocumentationReader } } + + override fun invoke(creationContext: AllModulesPageGeneration.DefaultAllModulesContext): RootPageNode { + val modules = context.configuration.modules + val sourceSetData = emptySet<DokkaSourceSet>() + val builder = PageContentBuilder(commentsConverter, signatureProvider, context.logger) + val contentNode = builder.contentFor( + dri = DRI(MULTIMODULE_PACKAGE_PLACEHOLDER), + kind = ContentKind.Cover, + sourceSets = sourceSetData + ) { + getMultiModuleDocumentation(context.configuration.includes).takeIf { it.isNotEmpty() }?.let { nodes -> + group(kind = ContentKind.Cover) { + nodes.forEach { node -> + group { + node.children.forEach { comment(it.root) } + } + } + } + } + header(2, "All modules:") + table(styles = setOf(MultimoduleTable)) { + header { group { text("Name") } } + modules.filter { it.name in creationContext.nonEmptyModules }.sortedBy { it.name } + .forEach { module -> + val displayedModuleDocumentation = getDisplayedModuleDocumentation(module) + val dri = DRI(packageName = MULTIMODULE_PACKAGE_PLACEHOLDER, classNames = module.name) + val dci = DCI(setOf(dri), ContentKind.Comment) + val extraWithAnchor = PropertyContainer.withAll(SymbolAnchorHint(module.name, ContentKind.Main)) + row(setOf(dri), emptySet(), styles = emptySet(), extra = extraWithAnchor) { + +linkNode(module.name, dri, DCI(setOf(dri), ContentKind.Main), extra = extraWithAnchor) + +ContentGroup( + children = + if (displayedModuleDocumentation != null) + DocTagToContentConverter().buildContent( + displayedModuleDocumentation, + dci, + emptySet() + ) + else emptyList(), + dci = dci, + sourceSets = emptySet(), + style = emptySet() + ) + } + } + } + } + return MultimoduleRootPageNode( + setOf(MULTIMODULE_ROOT_DRI), + contentNode + ) + } + + private fun getMultiModuleDocumentation(files: Set<File>): List<DocumentationNode> = + files.map { MarkdownParser({ null }, it.absolutePath).parse(it.readText()) } + + private fun getDisplayedModuleDocumentation(module: DokkaModuleDescription): P? { + return moduleDocumentationReader.read(module)?.firstParagraph() + } + + private fun DocumentationNode.firstParagraph(): P? = + this.children + .map { it.root } + .mapNotNull { it.firstParagraph() } + .firstOrNull() + + /** + * @return The very first, most inner paragraph. If any [P] is wrapped inside another [P], the inner one + * is preferred. + */ + private fun DocTag.firstParagraph(): P? { + val firstChildParagraph = children.mapNotNull { it.firstParagraph() }.firstOrNull() + return if (firstChildParagraph == null && this is P) this + else firstChildParagraph + } + + public companion object { + public const val MULTIMODULE_PACKAGE_PLACEHOLDER: String = ".ext" + public val MULTIMODULE_ROOT_DRI: DRI = + DRI(packageName = MULTIMODULE_PACKAGE_PLACEHOLDER, classNames = "allModules") + } +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ResolveLinkCommandHandler.kt b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ResolveLinkCommandHandler.kt new file mode 100644 index 00000000..f6d34271 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ResolveLinkCommandHandler.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.base.templating.Command +import org.jetbrains.dokka.base.templating.ResolveLinkCommand +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.templates.CommandHandler +import org.jsoup.nodes.Attributes +import org.jsoup.nodes.Element +import org.jsoup.parser.Tag +import java.io.File + +public class ResolveLinkCommandHandler(context: DokkaContext) : CommandHandler { + + private val externalModuleLinkResolver = + context.plugin<AllModulesPagePlugin>().querySingle { externalModuleLinkResolver } + + override fun handleCommandAsTag(command: Command, body: Element, input: File, output: File) { + command as ResolveLinkCommand + val link = externalModuleLinkResolver.resolve(command.dri, output) + if (link == null) { + val children = body.childNodes().toList() + val attributes = Attributes().apply { + put("data-unresolved-link", command.dri.toString()) + } + val el = Element(Tag.valueOf("span"), "", attributes).apply { + children.forEach { ch -> appendChild(ch) } + } + body.replaceWith(el) + return + } + + val attributes = Attributes().apply { + put("href", link) + } + val children = body.childNodes().toList() + val el = Element(Tag.valueOf("a"), "", attributes).apply { + children.forEach { ch -> appendChild(ch) } + } + body.replaceWith(el) + } + + override fun canHandle(command: Command): Boolean = command is ResolveLinkCommand +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/dokka-subprojects/plugin-all-modules-page/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin new file mode 100644 index 00000000..f50db659 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin @@ -0,0 +1,5 @@ +# +# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +org.jetbrains.dokka.allModulesPage.AllModulesPagePlugin diff --git a/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/MultiModuleDokkaTestGenerator.kt b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/MultiModuleDokkaTestGenerator.kt new file mode 100644 index 00000000..f3548e4c --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/MultiModuleDokkaTestGenerator.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPlugin +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.testApi.testRunner.AbstractTest +import org.jetbrains.dokka.testApi.testRunner.DokkaTestGenerator +import org.jetbrains.dokka.testApi.testRunner.TestBuilder +import org.jetbrains.dokka.testApi.testRunner.TestMethods +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.LoggingLevel + +class MultiModuleDokkaTestGenerator( + configuration: DokkaConfiguration, + logger: DokkaLogger, + testMethods: MultiModuleTestMethods, + additionalPlugins: List<DokkaPlugin> = emptyList() +) : DokkaTestGenerator<MultiModuleTestMethods>( + configuration, + logger, + testMethods, + additionalPlugins + AllModulesPagePlugin() +) { + override fun generate() = with(testMethods) { + val dokkaGenerator = DokkaGenerator(configuration, logger) + + val context = + dokkaGenerator.initializePlugins(configuration, logger, additionalPlugins + AllModulesPagePlugin()) + pluginsSetupStage(context) + + val generation = context.single(CoreExtensions.generation) as AllModulesPageGeneration + + val generationContext = generation.processSubmodules() + submoduleProcessingStage(context) + + val allModulesPage = generation.createAllModulesPage(generationContext) + allModulesPageCreationStage(allModulesPage) + + val transformedPages = generation.transformAllModulesPage(allModulesPage) + pagesTransformationStage(transformedPages) + + generation.render(transformedPages) + renderingStage(transformedPages, context) + + generation.processMultiModule(transformedPages) + processMultiModule(transformedPages) + + generation.finishProcessingSubmodules() + finishProcessingSubmodules(context) + } + +} + +open class MultiModuleTestMethods( + open val pluginsSetupStage: (DokkaContext) -> Unit, + open val allModulesPageCreationStage: (RootPageNode) -> Unit, + open val pagesTransformationStage: (RootPageNode) -> Unit, + open val renderingStage: (RootPageNode, DokkaContext) -> Unit, + open val submoduleProcessingStage: (DokkaContext) -> Unit, + open val processMultiModule: (RootPageNode) -> Unit, + open val finishProcessingSubmodules: (DokkaContext) -> Unit, +) : TestMethods + +class MultiModuleTestBuilder : TestBuilder<MultiModuleTestMethods>() { + var pluginsSetupStage: (DokkaContext) -> Unit = {} + var allModulesPageCreationStage: (RootPageNode) -> Unit = {} + var pagesTransformationStage: (RootPageNode) -> Unit = {} + var renderingStage: (RootPageNode, DokkaContext) -> Unit = { _, _ -> } + var submoduleProcessingStage: (DokkaContext) -> Unit = {} + var processMultiModule: (RootPageNode) -> Unit = {} + var finishProcessingSubmodules: (DokkaContext) -> Unit = {} + + override fun build() = MultiModuleTestMethods( + pluginsSetupStage, + allModulesPageCreationStage, + pagesTransformationStage, + renderingStage, + submoduleProcessingStage, + processMultiModule, + finishProcessingSubmodules + ) +} + +abstract class MultiModuleAbstractTest(logger: TestLogger = TestLogger(DokkaConsoleLogger(LoggingLevel.DEBUG))) : + AbstractTest<MultiModuleTestMethods, MultiModuleTestBuilder, MultiModuleDokkaTestGenerator>( + ::MultiModuleTestBuilder, + ::MultiModuleDokkaTestGenerator, + logger, + ) diff --git a/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/MultiModuleDocumentationTest.kt b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/MultiModuleDocumentationTest.kt new file mode 100644 index 00000000..3d9636ef --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/MultiModuleDocumentationTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage.templates + +import matchers.content.* +import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest +import org.jetbrains.dokka.model.dfs +import org.jetbrains.dokka.pages.ContentKind +import org.jetbrains.dokka.pages.ContentResolvedLink +import org.jetbrains.dokka.pages.MultimoduleRootPageNode +import org.junit.jupiter.api.io.TempDir +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class MultiModuleDocumentationTest : MultiModuleAbstractTest() { + + @field:TempDir + lateinit var tempDir: File + + val documentationContent = """ + # Sample project + Sample documentation with [external link](https://www.google.pl) + """.trimIndent() + + @BeforeTest + fun setup() { + tempDir.resolve("README.md").writeText(documentationContent) + } + + @AfterTest + fun teardown(){ + tempDir.resolve("README.md").delete() + } + + @Test + fun `documentation should be included in all modules page`() { + val configuration = dokkaConfiguration { + includes = listOf(tempDir.resolve("README.md")) + } + + testFromData(configuration) { + allModulesPageCreationStage = { rootPage -> + (rootPage as? MultimoduleRootPageNode)?.content?.dfs { it.dci.kind == ContentKind.Cover }?.children?.firstOrNull() + ?.assertNode { + group { + group { + group { + header(1) { + +"Sample project" + } + group { + +"Sample documentation with " + link { + +"external link" + check { + assertEquals( + "https://www.google.pl", + (this as ContentResolvedLink).address + ) + } + } + } + } + } + } + } + } + } + } +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt new file mode 100644 index 00000000..32b06a5b --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage.templates + +import kotlinx.html.a +import kotlinx.html.span +import kotlinx.html.stream.createHTML +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest +import org.jetbrains.dokka.base.renderers.html.templateCommand +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.base.templating.ResolveLinkCommand +import org.jetbrains.dokka.links.DRI +import org.junit.jupiter.api.io.TempDir +import utils.assertHtmlEqualsIgnoringWhitespace +import java.io.File +import kotlin.test.Test +import kotlin.test.assertTrue + +class ResolveLinkCommandResolutionTest : MultiModuleAbstractTest() { + + @Test + fun `should resolve link to another module`(@TempDir outputDirectory: File) { + val testedDri = DRI( + packageName = "package2", + classNames = "Sample", + ) + val link = createHTML().templateCommand(ResolveLinkCommand(testedDri)) { + span { + +"Sample" + } + } + + val expected = createHTML().a { + href = "../module2/package2/-sample/index.html" + span { + +"Sample" + } + } + + val contentFile = setup(outputDirectory, link) + val configuration = createConfiguration(outputDirectory) + + testFromData(configuration, useOutputLocationFromConfig = true) { + finishProcessingSubmodules = { + assertHtmlEqualsIgnoringWhitespace(expected, contentFile.readText()) + } + } + } + + @Test + fun `should produce content when link is not resolvable`(@TempDir outputDirectory: File) { + val testedDri = DRI( + packageName = "not-resolvable-package", + classNames = "Sample", + ) + val link = createHTML().templateCommand(ResolveLinkCommand(testedDri)) { + span { + +"Sample" + } + } + + val expected = createHTML().span { + attributes["data-unresolved-link"] = testedDri.toString() + span { + +"Sample" + } + } + + val contentFile = setup(outputDirectory, link) + val configuration = createConfiguration(outputDirectory) + + testFromData(configuration, useOutputLocationFromConfig = true) { + finishProcessingSubmodules = { + assertHtmlEqualsIgnoringWhitespace(expected, contentFile.readText()) + } + } + } + + private fun setup(outputDirectory: File, content: String): File { + val innerModule1 = outputDirectory.resolve("module1").also { assertTrue(it.mkdirs()) } + val innerModule2 = outputDirectory.resolve("module2").also { assertTrue(it.mkdirs()) } + val packageList = innerModule2.resolve("package-list") + packageList.writeText(mockedPackageListForPackages(RecognizedLinkFormat.DokkaHtml, "package2")) + val contentFile = innerModule1.resolve("index.html") + contentFile.writeText(content) + return contentFile + } + + private fun createConfiguration(outputDirectory: File) = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = outputDirectory.resolve("module1"), + includes = emptySet(), + sourceOutputDirectory = outputDirectory.resolve("module1"), + ), + DokkaModuleDescriptionImpl( + name = "module2", + relativePathToOutputDirectory = outputDirectory.resolve("module2"), + includes = emptySet(), + sourceOutputDirectory = outputDirectory.resolve("module2"), + ) + ) + this.outputDir = outputDirectory + } +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt new file mode 100644 index 00000000..b17d6765 --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage.templates + +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.allModulesPage.MultiModuleAbstractTest +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.gfm.GfmCommand.Companion.templateCommand +import org.jetbrains.dokka.gfm.GfmPlugin +import org.jetbrains.dokka.gfm.ResolveLinkGfmCommand +import org.jetbrains.dokka.gfm.templateProcessing.GfmTemplateProcessingPlugin +import org.jetbrains.dokka.links.DRI +import org.junit.jupiter.api.io.TempDir +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ResolveLinkGfmCommandResolutionTest : MultiModuleAbstractTest() { + + @Test + fun `should resolve link to another module`(@TempDir outputDirectory: File) { + val configuration = dokkaConfiguration { + modules = listOf( + DokkaModuleDescriptionImpl( + name = "module1", + relativePathToOutputDirectory = outputDirectory.resolve("module1"), + includes = emptySet(), + sourceOutputDirectory = outputDirectory.resolve("module1"), + ), + DokkaModuleDescriptionImpl( + name = "module2", + relativePathToOutputDirectory = outputDirectory.resolve("module2"), + includes = emptySet(), + sourceOutputDirectory = outputDirectory.resolve("module2"), + ) + ) + outputDir = outputDirectory + } + + val innerModule1 = outputDirectory.resolve("module1").also { assertTrue(it.mkdirs()) } + val innerModule2 = outputDirectory.resolve("module2").also { assertTrue(it.mkdirs()) } + + val indexMd = innerModule1.resolve("index.md") + val packageList = innerModule2.resolve("package-list") + + val indexMdContent = StringBuilder().apply { + templateCommand( + ResolveLinkGfmCommand( + dri = DRI( + packageName = "package2", + classNames = "Sample", + ) + ) + ) { + append("Sample text inside") + } + }.toString() + + indexMd.writeText(indexMdContent) + packageList.writeText(mockedPackageListForPackages(RecognizedLinkFormat.DokkaGFM, "package2")) + + testFromData( + configuration, + pluginOverrides = listOf(GfmTemplateProcessingPlugin(), GfmPlugin()), + useOutputLocationFromConfig = true + ) { + finishProcessingSubmodules = { + val expectedIndexMd = "[Sample text inside](../module2/package2/-sample/index.md)" + assertEquals(expectedIndexMd, indexMd.readText().trim()) + } + } + } +} diff --git a/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt new file mode 100644 index 00000000..e4ee4eaa --- /dev/null +++ b/dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.allModulesPage.templates + +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat + +internal fun mockedPackageListForPackages(format: RecognizedLinkFormat, vararg packages: String): String = + """ + ${PackageList.DOKKA_PARAM_PREFIX}.format:${format.formatName} + ${PackageList.DOKKA_PARAM_PREFIX}.linkExtension:${format.linkExtension} + + ${packages.sorted().joinToString(separator = "\n", postfix = "\n") { it }} + """.trimIndent() |