aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-all-modules-page
diff options
context:
space:
mode:
Diffstat (limited to 'dokka-subprojects/plugin-all-modules-page')
-rw-r--r--dokka-subprojects/plugin-all-modules-page/README.md7
-rw-r--r--dokka-subprojects/plugin-all-modules-page/api/plugin-all-modules-page.api87
-rw-r--r--dokka-subprojects/plugin-all-modules-page/build.gradle.kts32
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPageGeneration.kt80
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/AllModulesPagePlugin.kt58
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ExternalModuleLinkResolver.kt79
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimoduleLocationProvider.kt59
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/MultimodulePageCreator.kt115
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/kotlin/org/jetbrains/dokka/allModulesPage/ResolveLinkCommandHandler.kt49
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin5
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/MultiModuleDokkaTestGenerator.kt98
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/MultiModuleDocumentationTest.kt75
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkCommandResolutionTest.kt109
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/ResolveLinkGfmCommandResolutionTest.kt76
-rw-r--r--dokka-subprojects/plugin-all-modules-page/src/test/kotlin/org/jetbrains/dokka/allModulesPage/templates/mockedPackageListFactory.kt16
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()