From fc5496fb8bb888c3a50717260dc05d8fe2d54306 Mon Sep 17 00:00:00 2001
From: vmishenev <vad-mishenev@yandex.ru>
Date: Wed, 1 Sep 2021 00:03:13 +0300
Subject: Improve versioning plugin (#2104)

- support for single module projects
- version navigator is on all pages
- dropdown arrow for version navigator
---
 .../DefaultPreviousDocumentationCopyPostAction.kt  |  54 +++++++++++
 .../versioning/ReplaceVersionCommandConsumer.kt    |  13 ++-
 .../kotlin/versioning/ReplaceVersionsCommand.kt    |   3 +-
 .../kotlin/versioning/VersioningConfiguration.kt   |   5 +
 .../main/kotlin/versioning/VersioningHandler.kt    | 104 ---------------------
 .../src/main/kotlin/versioning/VersioningPlugin.kt |  25 +++--
 .../main/kotlin/versioning/VersioningStorage.kt    |  65 +++++++++++++
 .../kotlin/versioning/VersionsNavigationCreator.kt |  73 +++++++++++----
 .../main/kotlin/versioning/htmlPreprocessors.kt    |  22 ++++-
 9 files changed, 224 insertions(+), 140 deletions(-)
 create mode 100644 plugins/versioning/src/main/kotlin/versioning/DefaultPreviousDocumentationCopyPostAction.kt
 delete mode 100644 plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt
 create mode 100644 plugins/versioning/src/main/kotlin/versioning/VersioningStorage.kt

(limited to 'plugins/versioning/src/main/kotlin')

diff --git a/plugins/versioning/src/main/kotlin/versioning/DefaultPreviousDocumentationCopyPostAction.kt b/plugins/versioning/src/main/kotlin/versioning/DefaultPreviousDocumentationCopyPostAction.kt
new file mode 100644
index 00000000..6ab81d31
--- /dev/null
+++ b/plugins/versioning/src/main/kotlin/versioning/DefaultPreviousDocumentationCopyPostAction.kt
@@ -0,0 +1,54 @@
+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.renderers.PostAction
+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.TemplateProcessingStrategy
+import org.jetbrains.dokka.templates.TemplatingPlugin
+import java.io.File
+
+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)
+        }
+    }
+}
\ No newline at end of file
diff --git a/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt
index 2577b2da..ea524c52 100644
--- a/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt
+++ b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionCommandConsumer.kt
@@ -8,6 +8,7 @@ import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolu
 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
@@ -16,6 +17,8 @@ class ReplaceVersionCommandConsumer(context: DokkaContext) : ImmediateHtmlComman
 
     private val versionsNavigationCreator =
         context.plugin<VersioningPlugin>().querySingle { versionsNavigationCreator }
+    private val versioningStorage =
+        context.plugin<VersioningPlugin>().querySingle { versioningStorage }
 
     override fun canProcess(command: Command) = command is ReplaceVersionsCommand
 
@@ -27,7 +30,7 @@ class ReplaceVersionCommandConsumer(context: DokkaContext) : ImmediateHtmlComman
         command as ReplaceVersionsCommand
         templateCommandFor(command, tagConsumer).visit {
             unsafe {
-                +versionsNavigationCreator()
+                +versionsNavigationCreator(versioningStorage.currentVersion.dir.resolve(command.location))
             }
         }
     }
@@ -36,10 +39,12 @@ class ReplaceVersionCommandConsumer(context: DokkaContext) : ImmediateHtmlComman
         command: Command,
         block: TemplateBlock,
         tagConsumer: ImmediateResolutionTagConsumer<R>
-    ): R =
-        templateCommandFor(command, tagConsumer).visitAndFinalize(tagConsumer) {
+    ): R {
+        command as ReplaceVersionsCommand
+        return templateCommandFor(command, tagConsumer).visitAndFinalize(tagConsumer) {
             unsafe {
-                +versionsNavigationCreator()
+                +versionsNavigationCreator(versioningStorage.currentVersion.dir.resolve(command.location))
             }
         }
+    }
 }
\ No newline at end of file
diff --git a/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt
index 2d2c8e36..728eac09 100644
--- a/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt
+++ b/plugins/versioning/src/main/kotlin/versioning/ReplaceVersionsCommand.kt
@@ -2,6 +2,7 @@ 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
@@ -9,8 +10,6 @@ import org.jetbrains.dokka.templates.CommandHandler
 import org.jsoup.nodes.Element
 import java.io.File
 
-object ReplaceVersionsCommand : Command
-
 class ReplaceVersionCommandHandler(context: DokkaContext) : CommandHandler {
 
     val versionsNavigationCreator by lazy {
diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt
index f5c69cf4..56c72257 100644
--- a/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt
+++ b/plugins/versioning/src/main/kotlin/versioning/VersioningConfiguration.kt
@@ -9,6 +9,7 @@ data class VersioningConfiguration(
     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"
@@ -25,5 +26,9 @@ data class VersioningConfiguration(
         val defaultOlderVersions: List<File>? = null
         val defaultVersionsOrdering: List<String>? = null
         val defaultVersion = null
+        val defaultRenderVersionsNavigationOnAllPages = true
+
+        const val OLDER_VERSIONS_DIR = "older"
+        const val VERSIONS_FILE = "version.json"
     }
 }
\ No newline at end of file
diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt
deleted file mode 100644
index 1cc584b7..00000000
--- a/plugins/versioning/src/main/kotlin/versioning/VersioningHandler.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-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 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.configuration
-import org.jetbrains.dokka.plugability.plugin
-import org.jetbrains.dokka.plugability.query
-import org.jetbrains.dokka.templates.TemplateProcessingStrategy
-import org.jetbrains.dokka.templates.TemplatingPlugin
-import java.io.File
-
-interface VersioningHandler : () -> Unit {
-    fun getVersions(): Map<VersionId, File>
-    fun currentVersion(): File?
-}
-
-typealias VersionId = String
-
-class DefaultVersioningHandler(val context: DokkaContext) : VersioningHandler {
-
-    private val mapper = ObjectMapper()
-
-    private lateinit var versions: Map<VersionId, File>
-
-    private val processingStrategies: List<TemplateProcessingStrategy> =
-        context.plugin<TemplatingPlugin>().query { templateProcessingStrategy }
-
-    private val configuration = configuration<VersioningPlugin, VersioningConfiguration>(context)
-
-    override fun getVersions() = versions
-
-    override fun currentVersion() = configuration?.let { versionsConfiguration ->
-        versions[versionsConfiguration.versionFromConfigurationOrModule(context)]
-    }
-
-    override fun invoke() {
-        configuration?.let { versionsConfiguration ->
-            versions =
-                mapOf(versionsConfiguration.versionFromConfigurationOrModule(context) to context.configuration.outputDir)
-            handlePreviousVersions(versionsConfiguration.allOlderVersions(), context.configuration.outputDir)
-            mapper.writeValue(
-                context.configuration.outputDir.resolve(VERSIONS_FILE),
-                Version(versionsConfiguration.versionFromConfigurationOrModule(context))
-            )
-        }
-    }
-
-    private fun handlePreviousVersions(olderVersions: List<File>, output: File): Map<String, File> {
-        return versionsFrom(olderVersions)
-            .also { fetched ->
-                versions = versions + fetched.map { (key, _) ->
-                    key to output.resolve(OLDER_VERSIONS_DIR).resolve(key)
-                }.toMap()
-            }
-            .onEach { (version, path) -> copyVersion(version, path, output) }.toMap()
-    }
-
-    private fun versionsFrom(olderVersions: List<File>) =
-        olderVersions.mapNotNull { versionDir ->
-            versionDir.listFiles { _, name -> name == 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 $VERSIONS_FILE in $versionDir")
-            }
-        }
-
-    private fun copyVersion(version: VersionId, versionRoot: File, output: File) {
-        val targetParent = output.resolve(OLDER_VERSIONS_DIR).resolve(version).apply { mkdirs() }
-        val olderDirs = versionRoot.resolve(OLDER_VERSIONS_DIR)
-        runBlocking(Dispatchers.Default) {
-            coroutineScope {
-                versionRoot.listFiles().orEmpty()
-                    .filter { it.absolutePath != olderDirs.absolutePath }
-                    .forEach { versionRootContent ->
-                        launch {
-                            if (versionRootContent.isDirectory) versionRootContent.copyRecursively(
-                                targetParent.resolve(versionRootContent.name),
-                                overwrite = true
-                            )
-                            else processingStrategies.first {
-                                it.process(versionRootContent, targetParent.resolve(versionRootContent.name), null)
-                            }
-                        }
-                    }
-            }
-        }
-    }
-
-    private data class Version(
-        @JsonProperty("version") val version: String,
-    )
-
-    companion object {
-        private const val OLDER_VERSIONS_DIR = "older"
-        private const val VERSIONS_FILE = "version.json"
-    }
-}
diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt
index 9c20a128..e4e8bbae 100644
--- a/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt
+++ b/plugins/versioning/src/main/kotlin/versioning/VersioningPlugin.kt
@@ -1,5 +1,6 @@
 package org.jetbrains.dokka.versioning
 
+import org.jetbrains.dokka.CoreExtensions.postActions
 import org.jetbrains.dokka.base.DokkaBase
 import org.jetbrains.dokka.plugability.DokkaPlugin
 import org.jetbrains.dokka.plugability.configuration
@@ -7,27 +8,36 @@ import org.jetbrains.dokka.templates.TemplatingPlugin
 
 class VersioningPlugin : DokkaPlugin() {
 
-    val versioningHandler by extensionPoint<VersioningHandler>()
+    val versioningStorage by extensionPoint<VersioningStorage>()
     val versionsNavigationCreator by extensionPoint<VersionsNavigationCreator>()
     val versionsOrdering by extensionPoint<VersionsOrdering>()
 
     private val dokkaBase by lazy { plugin<DokkaBase>() }
     private val templatingPlugin by lazy { plugin<TemplatingPlugin>() }
 
-    val defaultVersioningHandler by extending {
-        versioningHandler providing ::DefaultVersioningHandler
+    val defaultVersioningStorage by extending {
+        versioningStorage providing ::DefaultVersioningStorage
     }
     val defaultVersioningNavigationCreator by extending {
         versionsNavigationCreator providing ::HtmlVersionsNavigationCreator
     }
     val replaceVersionCommandHandler by extending {
-        templatingPlugin.directiveBasedCommandHandlers providing ::ReplaceVersionCommandHandler
+        templatingPlugin.directiveBasedCommandHandlers providing ::ReplaceVersionCommandHandler override templatingPlugin.replaceVersionCommandHandler
     }
     val resolveLinkConsumer by extending {
-        dokkaBase.immediateHtmlCommandConsumer providing ::ReplaceVersionCommandConsumer
+        dokkaBase.immediateHtmlCommandConsumer providing ::ReplaceVersionCommandConsumer override dokkaBase.replaceVersionConsumer
     }
     val cssStyleInstaller by extending {
-        dokkaBase.htmlPreprocessors with MultiModuleStylesInstaller order { after(dokkaBase.assetsInstaller) }
+        dokkaBase.htmlPreprocessors providing ::MultiModuleStylesInstaller order {
+            after(dokkaBase.assetsInstaller)
+            before(dokkaBase.customResourceInstaller)
+        }
+    }
+    val notFoundPageInstaller by extending {
+        dokkaBase.htmlPreprocessors providing ::NotFoundPageInstaller order {
+            after(dokkaBase.assetsInstaller)
+            before(dokkaBase.customResourceInstaller)
+        } applyIf { !delayTemplateSubstitution }
     }
     val versionsDefaultOrdering by extending {
         versionsOrdering providing { ctx ->
@@ -36,4 +46,7 @@ class VersioningPlugin : DokkaPlugin() {
             } ?: SemVerVersionOrdering()
         }
     }
+    val previousDocumentationCopyPostAction by extending {
+        postActions providing ::DefaultPreviousDocumentationCopyPostAction applyIf { !delayTemplateSubstitution }
+    }
 }
\ No newline at end of file
diff --git a/plugins/versioning/src/main/kotlin/versioning/VersioningStorage.kt b/plugins/versioning/src/main/kotlin/versioning/VersioningStorage.kt
new file mode 100644
index 00000000..6e1dd7ca
--- /dev/null
+++ b/plugins/versioning/src/main/kotlin/versioning/VersioningStorage.kt
@@ -0,0 +1,65 @@
+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
+
+data class VersionDirs(val src: File, val dst: File)
+data class CurrentVersion(val name: String, val dir: File)
+
+interface VersioningStorage {
+    val previousVersions: Map<VersionId, VersionDirs>
+    val currentVersion: CurrentVersion
+    fun createVersionFile()
+}
+
+typealias VersionId = String
+
+class DefaultVersioningStorage(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/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt b/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt
index 76653d47..876b3ccf 100644
--- a/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt
+++ b/plugins/versioning/src/main/kotlin/versioning/VersionsNavigationCreator.kt
@@ -1,53 +1,86 @@
 package org.jetbrains.dokka.versioning
 
 import kotlinx.html.a
-import kotlinx.html.button
 import kotlinx.html.div
-import kotlinx.html.i
 import kotlinx.html.stream.appendHTML
+import org.jetbrains.dokka.base.renderers.html.strike
 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
-import java.nio.file.Files.isDirectory
-import java.nio.file.Path
 
 interface VersionsNavigationCreator {
-    operator fun invoke(): String
     operator fun invoke(output: File): String
 }
 
-class HtmlVersionsNavigationCreator(val context: DokkaContext) : VersionsNavigationCreator {
+class HtmlVersionsNavigationCreator(private val context: DokkaContext) : VersionsNavigationCreator {
 
-    private val versioningHandler by lazy { context.plugin<VersioningPlugin>().querySingle { versioningHandler } }
+    private val versioningStorage by lazy { context.plugin<VersioningPlugin>().querySingle { versioningStorage } }
 
     private val versionsOrdering by lazy { context.plugin<VersioningPlugin>().querySingle { versionsOrdering } }
 
-    override fun invoke(): String =
-        versioningHandler.currentVersion()?.let { invoke(it) }.orEmpty()
+    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
-        return versioningHandler.getVersions()
+        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 { versions ->
+            ?.let { orderedVersions ->
                 StringBuilder().appendHTML().div(classes = "versions-dropdown") {
-                    button(classes = "versions-dropdown-button") {
-                        versions.first { (_, versionLocation) -> versionLocation?.absolutePath == position.absolutePath }
-                            .let { (version, _) ->
-                                text(version)
-                            }
-                        i(classes = "fa fa-caret-down")
+                    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") {
-                        versions.forEach { (version, path) ->
-                            a(href = path?.resolve("index.html")?.toRelativeString(position)) {
-                                text(version)
+                        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 }
+        }
 }
\ No newline at end of file
diff --git a/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt b/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt
index 5852ba9e..94400898 100644
--- a/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt
+++ b/plugins/versioning/src/main/kotlin/versioning/htmlPreprocessors.kt
@@ -3,22 +3,36 @@ 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
 
-object MultiModuleStylesInstaller : PageTransformer {
+class MultiModuleStylesInstaller(private val dokkaContext: DokkaContext) : PageTransformer {
     private val stylesPages = listOf(
         "styles/multimodule.css",
     )
 
     override fun invoke(input: RootPageNode): RootPageNode =
-        input.modified(
-            children = input.children + stylesPages.toRenderSpecificResourcePage()
-        ).transformContentPagesTree {
+        input.let { root ->
+            if (dokkaContext.configuration.delayTemplateSubstitution) root
+            else root.modified(children = input.children + stylesPages.toRenderSpecificResourcePage())
+        }.transformContentPagesTree {
             it.modified(
                 embeddedResources = it.embeddedResources + stylesPages
             )
         }
 }
 
+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")) }
-- 
cgit