aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-versioning/src
diff options
context:
space:
mode:
authorIgnat Beresnev <ignat.beresnev@jetbrains.com>2023-11-10 11:46:54 +0100
committerGitHub <noreply@github.com>2023-11-10 11:46:54 +0100
commit8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch)
tree1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-versioning/src
parenta44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff)
downloaddokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.gz
dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.bz2
dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.zip
Restructure the project to utilize included builds (#3174)
* Refactor and simplify artifact publishing * Update Gradle to 8.4 * Refactor and simplify convention plugins and build scripts Fixes #3132 --------- Co-authored-by: Adam <897017+aSemy@users.noreply.github.com> Co-authored-by: Oleg Yukhnevich <whyoleg@gmail.com>
Diffstat (limited to 'dokka-subprojects/plugin-versioning/src')
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/DefaultPreviousDocumentationCopyPostAction.kt60
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionCommandConsumer.kt54
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionsCommand.kt29
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningConfiguration.kt38
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningPlugin.kt70
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningStorage.kt72
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsNavigationCreator.kt91
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsOrdering.kt26
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/htmlPreprocessors.kt46
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin5
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/resources/dokka/not-found-version.html193
-rw-r--r--dokka-subprojects/plugin-versioning/src/main/resources/dokka/styles/multimodule.css55
12 files changed, 739 insertions, 0 deletions
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/DefaultPreviousDocumentationCopyPostAction.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/DefaultPreviousDocumentationCopyPostAction.kt
new file mode 100644
index 00000000..7e03f59c
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/DefaultPreviousDocumentationCopyPostAction.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+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.renderers.PostAction
+import org.jetbrains.dokka.templates.TemplateProcessingStrategy
+import org.jetbrains.dokka.templates.TemplatingPlugin
+import java.io.File
+
+public class DefaultPreviousDocumentationCopyPostAction(
+ private val context: DokkaContext
+) : PostAction {
+ private val versioningStorage by lazy { context.plugin<VersioningPlugin>().querySingle { versioningStorage } }
+ private val processingStrategies: List<TemplateProcessingStrategy> =
+ context.plugin<TemplatingPlugin>().query { templateProcessingStrategy }
+
+ override fun invoke() {
+ versioningStorage.createVersionFile()
+ versioningStorage.previousVersions.forEach { (_, dirs) -> copyVersion(dirs.src, dirs.dst) }
+ }
+
+ private fun copyVersion(versionRoot: File, targetParent: File) {
+ targetParent.apply { mkdirs() }
+ val ignoreDir = versionRoot.resolve(VersioningConfiguration.OLDER_VERSIONS_DIR)
+ runBlocking(Dispatchers.Default) {
+ coroutineScope {
+ versionRoot.listFiles().orEmpty()
+ .filter { it.absolutePath != ignoreDir.absolutePath }
+ .forEach { versionRootContent ->
+ launch {
+ processRecursively(versionRootContent, targetParent)
+ }
+ }
+ }
+ }
+ }
+
+ private fun processRecursively(versionRootContent: File, targetParent: File) {
+ if (versionRootContent.isDirectory) {
+ val target = targetParent.resolve(versionRootContent.name).also { it.mkdir() }
+ versionRootContent.listFiles()?.forEach {
+ processRecursively(it, target)
+ }
+ } else if (versionRootContent.extension == "html") processingStrategies.first {
+ it.process(versionRootContent, targetParent.resolve(versionRootContent.name), null)
+ } else {
+ versionRootContent.copyTo(targetParent.resolve(versionRootContent.name), overwrite = true)
+ }
+ }
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionCommandConsumer.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionCommandConsumer.kt
new file mode 100644
index 00000000..b31afb9a
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionCommandConsumer.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import kotlinx.html.unsafe
+import kotlinx.html.visit
+import kotlinx.html.visitAndFinalize
+import org.jetbrains.dokka.base.renderers.html.TemplateBlock
+import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
+import org.jetbrains.dokka.base.renderers.html.templateCommandFor
+import org.jetbrains.dokka.base.templating.Command
+import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer
+import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.plugin
+import org.jetbrains.dokka.plugability.querySingle
+
+public class ReplaceVersionCommandConsumer(context: DokkaContext) : ImmediateHtmlCommandConsumer {
+
+ private val versionsNavigationCreator =
+ context.plugin<VersioningPlugin>().querySingle { versionsNavigationCreator }
+ private val versioningStorage =
+ context.plugin<VersioningPlugin>().querySingle { versioningStorage }
+
+ override fun canProcess(command: Command): Boolean = command is ReplaceVersionsCommand
+
+ override fun <R> processCommand(
+ command: Command,
+ block: TemplateBlock,
+ tagConsumer: ImmediateResolutionTagConsumer<R>
+ ) {
+ command as ReplaceVersionsCommand
+ templateCommandFor(command, tagConsumer).visit {
+ unsafe {
+ +versionsNavigationCreator(versioningStorage.currentVersion.dir.resolve(command.location))
+ }
+ }
+ }
+
+ override fun <R> processCommandAndFinalize(
+ command: Command,
+ block: TemplateBlock,
+ tagConsumer: ImmediateResolutionTagConsumer<R>
+ ): R {
+ command as ReplaceVersionsCommand
+ return templateCommandFor(command, tagConsumer).visitAndFinalize(tagConsumer) {
+ unsafe {
+ +versionsNavigationCreator(versioningStorage.currentVersion.dir.resolve(command.location))
+ }
+ }
+ }
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionsCommand.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionsCommand.kt
new file mode 100644
index 00000000..c9bc57b2
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/ReplaceVersionsCommand.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+
+import org.jetbrains.dokka.base.templating.Command
+import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand
+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.Element
+import java.io.File
+
+public class ReplaceVersionCommandHandler(context: DokkaContext) : CommandHandler {
+
+ public val versionsNavigationCreator: VersionsNavigationCreator by lazy {
+ context.plugin<VersioningPlugin>().querySingle { versionsNavigationCreator }
+ }
+
+ override fun canHandle(command: Command): Boolean = command is ReplaceVersionsCommand
+
+ override fun handleCommandAsTag(command: Command, body: Element, input: File, output: File) {
+ body.empty()
+ body.append(versionsNavigationCreator(output))
+ }
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningConfiguration.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningConfiguration.kt
new file mode 100644
index 00000000..91b1117d
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningConfiguration.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import org.jetbrains.dokka.plugability.ConfigurableBlock
+import org.jetbrains.dokka.plugability.DokkaContext
+import java.io.File
+
+public data class VersioningConfiguration(
+ var olderVersionsDir: File? = defaultOlderVersionsDir,
+ var olderVersions: List<File>? = defaultOlderVersions,
+ var versionsOrdering: List<String>? = defaultVersionsOrdering,
+ var version: String? = defaultVersion,
+ var renderVersionsNavigationOnAllPages: Boolean? = defaultRenderVersionsNavigationOnAllPages
+) : ConfigurableBlock {
+ internal fun versionFromConfigurationOrModule(dokkaContext: DokkaContext): String =
+ version ?: dokkaContext.configuration.moduleVersion ?: "1.0"
+
+ internal fun allOlderVersions(): List<File> {
+ if (olderVersionsDir != null)
+ assert(olderVersionsDir!!.isDirectory) { "Supplied previous version $olderVersionsDir is not a directory!" }
+
+ return olderVersionsDir?.listFiles()?.toList().orEmpty() + olderVersions.orEmpty()
+ }
+
+ public companion object {
+ public val defaultOlderVersionsDir: File? = null
+ public val defaultOlderVersions: List<File>? = null
+ public val defaultVersionsOrdering: List<String>? = null
+ public val defaultVersion: String? = null
+ public val defaultRenderVersionsNavigationOnAllPages: Boolean = true
+
+ public const val OLDER_VERSIONS_DIR: String = "older"
+ public const val VERSIONS_FILE: String = "version.json"
+ }
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningPlugin.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningPlugin.kt
new file mode 100644
index 00000000..2e1fde8d
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningPlugin.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import org.jetbrains.dokka.CoreExtensions.postActions
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer
+import org.jetbrains.dokka.plugability.*
+import org.jetbrains.dokka.renderers.PostAction
+import org.jetbrains.dokka.templates.CommandHandler
+import org.jetbrains.dokka.templates.TemplatingPlugin
+import org.jetbrains.dokka.transformers.pages.PageTransformer
+
+public class VersioningPlugin : DokkaPlugin() {
+
+ public val versioningStorage: ExtensionPoint<VersioningStorage> by extensionPoint()
+ public val versionsNavigationCreator: ExtensionPoint<VersionsNavigationCreator> by extensionPoint()
+ public val versionsOrdering: ExtensionPoint<VersionsOrdering> by extensionPoint()
+
+ private val dokkaBase by lazy { plugin<DokkaBase>() }
+ private val templatingPlugin by lazy { plugin<TemplatingPlugin>() }
+
+ public val defaultVersioningStorage: Extension<VersioningStorage, *, *> by extending {
+ versioningStorage providing ::DefaultVersioningStorage
+ }
+
+ public val defaultVersioningNavigationCreator: Extension<VersionsNavigationCreator, *, *> by extending {
+ versionsNavigationCreator providing ::HtmlVersionsNavigationCreator
+ }
+
+ public val replaceVersionCommandHandler: Extension<CommandHandler, *, *> by extending {
+ templatingPlugin.directiveBasedCommandHandlers providing ::ReplaceVersionCommandHandler override templatingPlugin.replaceVersionCommandHandler
+ }
+
+ public val resolveLinkConsumer: Extension<ImmediateHtmlCommandConsumer, *, *> by extending {
+ dokkaBase.immediateHtmlCommandConsumer providing ::ReplaceVersionCommandConsumer override dokkaBase.replaceVersionConsumer
+ }
+
+ public val cssStyleInstaller: Extension<PageTransformer, *, *> by extending {
+ dokkaBase.htmlPreprocessors providing ::MultiModuleStylesInstaller order {
+ after(dokkaBase.assetsInstaller)
+ before(dokkaBase.customResourceInstaller)
+ }
+ }
+
+ public val notFoundPageInstaller: Extension<PageTransformer, *, *> by extending {
+ dokkaBase.htmlPreprocessors providing ::NotFoundPageInstaller order {
+ after(dokkaBase.assetsInstaller)
+ before(dokkaBase.customResourceInstaller)
+ } applyIf { !delayTemplateSubstitution }
+ }
+
+ public val versionsDefaultOrdering: Extension<VersionsOrdering, *, *> by extending {
+ versionsOrdering providing { ctx ->
+ configuration<VersioningPlugin, VersioningConfiguration>(ctx)?.versionsOrdering?.let {
+ ByConfigurationVersionOrdering(ctx)
+ } ?: SemVerVersionOrdering()
+ }
+ }
+
+ public val previousDocumentationCopyPostAction: Extension<PostAction, *, *> by extending {
+ postActions providing ::DefaultPreviousDocumentationCopyPostAction applyIf { !delayTemplateSubstitution }
+ }
+
+ @OptIn(DokkaPluginApiPreview::class)
+ override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement =
+ PluginApiPreviewAcknowledgement
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningStorage.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningStorage.kt
new file mode 100644
index 00000000..7c9d1da0
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersioningStorage.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+import java.io.File
+
+public data class VersionDirs(val src: File, val dst: File)
+public data class CurrentVersion(val name: String, val dir: File)
+
+public interface VersioningStorage {
+ public val previousVersions: Map<VersionId, VersionDirs>
+ public val currentVersion: CurrentVersion
+
+ public fun createVersionFile()
+}
+
+public typealias VersionId = String
+
+public class DefaultVersioningStorage(
+ public val context: DokkaContext
+) : VersioningStorage {
+
+ private val mapper = ObjectMapper()
+ private val configuration = configuration<VersioningPlugin, VersioningConfiguration>(context)
+
+ override val previousVersions: Map<VersionId, VersionDirs> by lazy {
+ configuration?.let { versionsConfiguration ->
+ getPreviousVersions(versionsConfiguration.allOlderVersions(), context.configuration.outputDir)
+ } ?: emptyMap()
+ }
+
+ override val currentVersion: CurrentVersion by lazy {
+ configuration?.let { versionsConfiguration ->
+ CurrentVersion(versionsConfiguration.versionFromConfigurationOrModule(context),
+ context.configuration.outputDir)
+ }?: CurrentVersion(context.configuration.moduleVersion.orEmpty(), context.configuration.outputDir)
+ }
+
+ override fun createVersionFile() {
+ mapper.writeValue(
+ currentVersion.dir.resolve(VersioningConfiguration.VERSIONS_FILE),
+ Version(currentVersion.name)
+ )
+ }
+
+ private fun getPreviousVersions(olderVersions: List<File>, output: File): Map<String, VersionDirs> =
+ versionsFrom(olderVersions).associate { (key, srcDir) ->
+ key to VersionDirs(srcDir, output.resolve(VersioningConfiguration.OLDER_VERSIONS_DIR).resolve(key))
+ }
+
+ private fun versionsFrom(olderVersions: List<File>) =
+ olderVersions.mapNotNull { versionDir ->
+ versionDir.listFiles { _, name -> name == VersioningConfiguration.VERSIONS_FILE }?.firstOrNull()
+ ?.let { file ->
+ val versionsContent = mapper.readValue<Version>(file)
+ Pair(versionsContent.version, versionDir)
+ }.also {
+ if (it == null) context.logger.warn("Failed to find versions file named ${VersioningConfiguration.VERSIONS_FILE} in $versionDir")
+ }
+ }
+
+ private data class Version(
+ @JsonProperty("version") val version: String,
+ )
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsNavigationCreator.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsNavigationCreator.kt
new file mode 100644
index 00000000..59ce93e2
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsNavigationCreator.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import kotlinx.html.a
+import kotlinx.html.div
+import kotlinx.html.stream.appendHTML
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+import org.jetbrains.dokka.plugability.plugin
+import org.jetbrains.dokka.plugability.querySingle
+import org.jetbrains.dokka.utilities.urlEncoded
+import java.io.File
+
+public fun interface VersionsNavigationCreator {
+ public operator fun invoke(output: File): String
+}
+
+public class HtmlVersionsNavigationCreator(
+ private val context: DokkaContext
+) : VersionsNavigationCreator {
+
+ private val versioningStorage by lazy { context.plugin<VersioningPlugin>().querySingle { versioningStorage } }
+
+ private val versionsOrdering by lazy { context.plugin<VersioningPlugin>().querySingle { versionsOrdering } }
+
+ private val isOnlyOnRootPage =
+ configuration<VersioningPlugin, VersioningConfiguration>(context)?.renderVersionsNavigationOnAllPages == false
+
+ private val versions: Map<VersionId, File> by lazy {
+ versioningStorage.previousVersions.map { (k, v) -> k to v.dst }.toMap() +
+ (versioningStorage.currentVersion.name to versioningStorage.currentVersion.dir)
+ }
+
+ override fun invoke(output: File): String {
+ if (versions.size == 1) {
+ return versioningStorage.currentVersion.name
+ }
+ val position = output.takeIf { it.isDirectory } ?: output.parentFile
+ if (isOnlyOnRootPage) {
+ getActiveVersion(position)?.takeIf {
+ it.value == versioningStorage.currentVersion.dir
+ && it.value != position
+ }?.also { return@invoke it.key }
+ }
+ return versions
+ .let { versions -> versionsOrdering.order(versions.keys.toList()).map { it to versions[it] } }
+ .takeIf { it.isNotEmpty() }
+ ?.let { orderedVersions ->
+ StringBuilder().appendHTML().div(classes = "versions-dropdown") {
+ val activeVersion = getActiveVersion(position)
+ val relativePosition: String = activeVersion?.value?.let { output.toRelativeString(it) } ?: "index.html"
+ div(classes = "versions-dropdown-button") {
+ activeVersion?.key?.let { text(it) }
+ }
+ div(classes = "versions-dropdown-data") {
+ orderedVersions.forEach { (version, path) ->
+ if (version == activeVersion?.key) {
+ a(href = output.name) { text(version) }
+ } else {
+ val isExistsFile =
+ if (version == versioningStorage.currentVersion.name)
+ path?.resolve(relativePosition)?.exists() == true
+ else
+ versioningStorage.previousVersions[version]?.src?.resolve(relativePosition)
+ ?.exists() == true
+
+ val absolutePath =
+ if (isExistsFile)
+ path?.resolve(relativePosition)
+ else
+ versioningStorage.currentVersion.dir.resolve("not-found-version.html")
+
+ a(href = absolutePath?.toRelativeString(position) +
+ if (!isExistsFile) "?v=" + version.urlEncoded() else "") {
+ text(version)
+ }
+ }
+ }
+ }
+ }.toString()
+ }.orEmpty()
+ }
+
+ private fun getActiveVersion(position: File) =
+ versions.minByOrNull { (_, versionLocation) ->
+ versionLocation.let { position.toRelativeString(it).length }
+ }
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsOrdering.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsOrdering.kt
new file mode 100644
index 00000000..3d1fbe3d
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/VersionsOrdering.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import org.apache.maven.artifact.versioning.ComparableVersion
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+
+public fun interface VersionsOrdering {
+ public fun order(records: List<VersionId>): List<VersionId>
+}
+
+public class ByConfigurationVersionOrdering(
+ public val dokkaContext: DokkaContext
+) : VersionsOrdering {
+ override fun order(records: List<VersionId>): List<VersionId> =
+ configuration<VersioningPlugin, VersioningConfiguration>(dokkaContext)?.versionsOrdering
+ ?: throw IllegalStateException("Attempted to use a configuration ordering without providing configuration")
+}
+
+public class SemVerVersionOrdering : VersionsOrdering {
+ override fun order(records: List<VersionId>): List<VersionId> =
+ records.map { it to ComparableVersion(it) }.sortedByDescending { it.second }.map { it.first }
+}
diff --git a/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/htmlPreprocessors.kt b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/htmlPreprocessors.kt
new file mode 100644
index 00000000..9bdaf7d5
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/kotlin/org/jetbrains/dokka/versioning/htmlPreprocessors.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package org.jetbrains.dokka.versioning
+
+import org.jetbrains.dokka.pages.RendererSpecificResourcePage
+import org.jetbrains.dokka.pages.RenderingStrategy
+import org.jetbrains.dokka.pages.RootPageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.transformers.pages.PageTransformer
+
+public class MultiModuleStylesInstaller(
+ private val dokkaContext: DokkaContext
+) : PageTransformer {
+ private val stylesPages = listOf(
+ "styles/multimodule.css",
+ )
+
+ override fun invoke(input: RootPageNode): RootPageNode =
+ input.let { root ->
+ if (dokkaContext.configuration.delayTemplateSubstitution) root
+ else root.modified(children = input.children + stylesPages.toRenderSpecificResourcePage())
+ }.transformContentPagesTree {
+ it.modified(
+ embeddedResources = it.embeddedResources + stylesPages
+ )
+ }
+}
+
+public class NotFoundPageInstaller(
+ private val dokkaContext: DokkaContext
+) : PageTransformer {
+ private val notFoundPage = listOf(
+ "not-found-version.html",
+ )
+
+ override fun invoke(input: RootPageNode): RootPageNode =
+ input.let { root ->
+ if (dokkaContext.configuration.delayTemplateSubstitution) root
+ else root.modified(children = input.children + notFoundPage.toRenderSpecificResourcePage())
+ }
+}
+
+private fun List<String>.toRenderSpecificResourcePage(): List<RendererSpecificResourcePage> =
+ map { RendererSpecificResourcePage(it, emptyList(), RenderingStrategy.Copy("/dokka/$it")) }
diff --git a/dokka-subprojects/plugin-versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin b/dokka-subprojects/plugin-versioning/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin
new file mode 100644
index 00000000..2afa663b
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/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.versioning.VersioningPlugin
diff --git a/dokka-subprojects/plugin-versioning/src/main/resources/dokka/not-found-version.html b/dokka-subprojects/plugin-versioning/src/main/resources/dokka/not-found-version.html
new file mode 100644
index 00000000..36cf343d
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/resources/dokka/not-found-version.html
@@ -0,0 +1,193 @@
+<!--
+ ~ Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ -->
+
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Unavailable page</title>
+ <style>
+.article-content h1._big {
+ text-transform: uppercase;
+ color: #161616;
+ font-size: 54px;
+ font-weight: 800;
+ line-height: 45px;
+}
+.sub-title {
+ margin-bottom: 40px;
+ font-size: 20px;
+ font-weight: 400;
+ line-height: 30px;
+}
+.margin-top-vertical-unit-half {
+ margin-top: 25px;
+}
+.wt-row_size_m {
+ --wt-horizontal-layout-gutter: 16px;
+}
+.article-content {
+ color: #343434;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Droid Sans, Helvetica Neue, Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 25px;
+}
+.wt-container {
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ padding-left: 22px;
+ padding-right: 22px;
+ max-width: 1276px;
+}
+.wt-col-5 {
+ --wt-col-count: 5;
+}
+.wt-col-3 {
+ --wt-col-count: 3;
+}
+.page-404__logo {
+ position: relative;
+ display: flex;
+}
+.wt-row, .wt-row_wide {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+}
+[class*="wt-col"] {
+ -ms-flex-preferred-size: calc(8.33333%*var(--wt-col-count) - var(--wt-horizontal-layout-gutter)*2);
+ flex-basis: calc(8.33333%*var(--wt-col-count) - var(--wt-horizontal-layout-gutter)*2);
+ max-width: calc(8.33333%*var(--wt-col-count) - var(--wt-horizontal-layout-gutter)*2);
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.wt-row_size_m {
+ --wt-horizontal-layout-gutter: 16px;
+}
+[class*="wt-col"], [class*="wt-col"].wt-row {
+ margin-right: var(--wt-horizontal-layout-gutter);
+ margin-left: var(--wt-horizontal-layout-gutter);
+}
+.wt-row_justify_center {
+ -webkit-box-pack: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+}
+.page-404__logo .sprite-img._404 {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+.page-404__logo::before {
+ float: left;
+ padding-bottom: 100%;
+ content: "";
+}
+.page-404__beam {
+ position: absolute;
+ left: -50%;
+ top: -50%;
+ width: 587px;
+ height: 636px;
+ z-index: -1;
+}
+.heavy {
+ font-size: 14px;
+ font-weight: bold;
+}
+ </style>
+</head>
+<body>
+<script type="text/javascript">
+function getUrlParams(url) {
+
+ var queryString = url ? url.split('?')[1] : window.location.search.slice(1);
+ var obj = {};
+
+ if (queryString) {
+ queryString = queryString.split('#')[0];
+
+ var arr = queryString.split('&');
+
+ for (var i = 0; i < arr.length; i++) {
+ var a = arr[i].split('=');
+
+ var paramName = a[0];
+ var paramValue = typeof (a[1]) === 'undefined' ? true : a[1];
+
+ paramName = paramName.toLowerCase();
+ if (typeof paramValue === 'string') paramValue = paramValue.toLowerCase();
+
+ if (!obj[paramName]) {
+ obj[paramName] = paramValue;
+ }
+ }
+ }
+ return obj;
+}
+window.onload = function() {
+ document.getElementById("version").textContent = getUrlParams()['v']
+}
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+ style="position: absolute; width: 0; height: 0" id="__SVG_SPRITE_NODE__">
+ <symbol xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64" id="404">
+ <g>
+ <rect y="0" width="64" height="64"></rect>
+ <rect x="5.9" y="52" fill="#fff" width="24" height="4"></rect>
+ <text x="5" y="20" fill="#fff" class="heavy">NOT</text>
+ <text x="5" y="35" fill="#fff" class="heavy">FOUND</text>
+ </g>
+ </symbol>
+ <symbol xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="183 -134 978 1061"
+ id="page-404-beam">
+ <g style="opacity:0.49;">
+ <path style="fill-opacity:0;stroke:#D9D9D9;stroke-dasharray:10,3;"
+ d="M1117.5,424.5l-535-476l87,449L1117.5,424.5z"></path>
+ <path style="fill-opacity:0;stroke:#E1E1E1;stroke-dasharray:10,3;"
+ d="M1118.3,465.9c-23.3,0-42.2-18.9-42.2-42.2 s18.9-42.2,42.2-42.2c23.3,0,42.2,18.9,42.2,42.2S1141.6,465.9,1118.3,465.9z M583.6,34.4c-46.3,0-83.9-37.6-83.9-83.9 s37.6-83.9,83.9-83.9s83.9,37.6,83.9,83.9S630,34.4,583.6,34.4z M536.2,841.7c0,46.8-37.9,84.8-84.7,84.8s-84.7-37.9-84.7-84.8 s37.9-84.8,84.7-84.8S536.2,794.9,536.2,841.7z M273.9,263.1c-49.9,0-90.4-40.5-90.4-90.4s40.5-90.4,90.4-90.4s90.4,40.5,90.4,90.4 S323.8,263.1,273.9,263.1z"></path>
+ <path style="fill:#FFFFFF;fill-opacity:0;stroke:#CCCCCC;stroke-linejoin:round;"
+ d="M1138.3,460.8L494,916l-0.4-0.7 c-12.4,7.1-26.8,11.2-42.2,11.2c-46.8,0-84.8-37.9-84.8-84.8c0-34.5,20.6-64.1,50.2-77.4l638.2-348l-470.3-382L417,173.1 l222.9,172.2c13.4,5.4,22.8,18.5,22.8,33.8c0,20.2-16.3,36.5-36.5,36.5c-9.8,0-18.6-3.8-25.2-10.1L242.9,257.6 c-2.2-0.8-4.3-1.7-6.3-2.6l-2-0.8l0-0.1c-30.2-14.6-51.1-45.6-51.1-81.4c0-28.6,13.3-54.1,34.1-70.7l0-0.3l321.1-222.2l0.2,0 c13-8.2,28.3-13,44.8-13c25.1,0,47.7,11.1,63,28.6l497.7,495.4c9.8,7.7,16.2,19.7,16.2,33.2 C1160.5,439.7,1151.5,453.7,1138.3,460.8z"></path>
+ <path style="fill-opacity:0;stroke:#D9D9D9;stroke-dasharray:10,3;"
+ d="M451.5,849.5l219-452l-398-223L451.5,849.5z"></path>
+ <g>
+ <path style="fill:#CDCDCD;"
+ d="M608.5,58.4l-5.7-5.1l-3.1,6.9l-0.7-0.6l3.1-6.9l-0.1-0.1l0.4-0.8l6.5,5.8L608.5,58.4z"></path>
+ <path style="fill:#CDCDCD;"
+ d="M353.8,220.4l3.1,6.9l-0.9,0.1l-3.1-6.9l-0.2,0l-0.4-0.8l8.6-1l0.4,0.8L353.8,220.4z"></path>
+ <path style="fill:#CDCDCD;"
+ d="M1041.4,418.5l-7.2,4.9l-0.7-0.5l6.3-4.3l-6-4.7l0.8-0.5l6,4.7l0.1-0.1L1041.4,418.5z"></path>
+ <path style="fill:#CDCDCD;"
+ d="M528.6,699.3l-6.9,3.1l0,0.2l-0.8,0.4l-1-8.6l0.8-0.4l0.8,7.6l6.9-3.1L528.6,699.3z"></path>
+ </g>
+ </g>
+ </symbol>
+</svg>
+<div class="wt-container article-content">
+ <div class="wt-row wt-row_size_m wt-row_justify_center">
+ <div class="wt-col-3">
+ <div class="page-404__logo">
+ <svg class="sprite-img _404">
+ <use xlink:href="#404"></use>
+ </svg>
+ <svg class="page-404__beam">
+ <use xlink:href="#page-404-beam"></use>
+ </svg>
+ </div>
+ </div>
+ <div class="wt-col-5">
+ <h1 class="_big">uh-oh!</h1>
+ <div class="sub-title margin-top-vertical-unit-half">You are requesting a page that not
+ available in documentation version <span id="version"></span>
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+</html>
diff --git a/dokka-subprojects/plugin-versioning/src/main/resources/dokka/styles/multimodule.css b/dokka-subprojects/plugin-versioning/src/main/resources/dokka/styles/multimodule.css
new file mode 100644
index 00000000..91798c1d
--- /dev/null
+++ b/dokka-subprojects/plugin-versioning/src/main/resources/dokka/styles/multimodule.css
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+.versions-dropdown {
+ position: relative;
+}
+
+.versions-dropdown-button {
+ display: flex;
+ border: none;
+ cursor: pointer;
+ padding: 5px;
+}
+
+.versions-dropdown-button::after {
+ content: '';
+ -webkit-mask: url("../images/arrow_down.svg") no-repeat 50% 50%;
+ mask: url("../images/arrow_down.svg") no-repeat 50% 50%;
+ mask-size: auto;
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ background-color: #fff;
+ display: inline-block;
+ transform: rotate(90deg);
+ width: 24px;
+ height: 16px;
+}
+
+.versions-dropdown-data {
+ display: none;
+ position: absolute;
+ background-color: #27282c;
+ border: 1px solid hsla(0, 0%, 100%, .6);
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
+ z-index: 1;
+ overflow-y: auto;
+ max-height: 200px;
+ min-width: 50px;
+}
+
+.versions-dropdown-data > a {
+ display: block;
+ padding: 5px;
+ color: #fff;
+ text-decoration: none;
+}
+
+.versions-dropdown-data > a:hover {
+ background-color: hsla(0,0%,100%,.1)
+}
+
+.versions-dropdown:hover .versions-dropdown-data {
+ display: block;
+}