aboutsummaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorVadim Mishenev <vad-mishenev@yandex.ru>2022-03-10 12:33:28 +0300
committerGitHub <noreply@github.com>2022-03-10 12:33:28 +0300
commit597b365ad4512b63387e54d16ba00eb94dccb97d (patch)
tree463f77cb2b27b0664b03be444bba1cbcb10bcbce /plugins
parent8537f0f6fc0d84c0f9918db4f7db9be053d632a8 (diff)
downloaddokka-597b365ad4512b63387e54d16ba00eb94dccb97d.tar.gz
dokka-597b365ad4512b63387e54d16ba00eb94dccb97d.tar.bz2
dokka-597b365ad4512b63387e54d16ba00eb94dccb97d.zip
KT-50452 Make flexible html for customization (#2374)
Diffstat (limited to 'plugins')
-rw-r--r--plugins/base/api/base.api63
-rw-r--r--plugins/base/build.gradle.kts4
-rw-r--r--plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt4
-rw-r--r--plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt5
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt156
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/Tags.kt6
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt207
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt16
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt76
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt16
-rw-r--r--plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt5
-rw-r--r--plugins/base/src/main/resources/dokka/templates/base.ftl62
12 files changed, 487 insertions, 133 deletions
diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api
index a38c2cfd..101a00e9 100644
--- a/plugins/base/api/base.api
+++ b/plugins/base/api/base.api
@@ -71,27 +71,30 @@ public final class org/jetbrains/dokka/base/DokkaBaseConfiguration : org/jetbrai
public static final field mergeImplicitExpectActualDeclarationsDefault Z
public static final field separateInheritedMembersDefault Z
public fun <init> ()V
- public fun <init> (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;Z)V
- public synthetic fun <init> (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun <init> (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;)V
+ public synthetic fun <init> (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/util/List;
public final fun component3 ()Z
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Z
- public final fun copy (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;Z)Lorg/jetbrains/dokka/base/DokkaBaseConfiguration;
- public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/DokkaBaseConfiguration;Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZILjava/lang/Object;)Lorg/jetbrains/dokka/base/DokkaBaseConfiguration;
+ public final fun component6 ()Ljava/io/File;
+ public final fun copy (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;)Lorg/jetbrains/dokka/base/DokkaBaseConfiguration;
+ public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/DokkaBaseConfiguration;Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/DokkaBaseConfiguration;
public fun equals (Ljava/lang/Object;)Z
public final fun getCustomAssets ()Ljava/util/List;
public final fun getCustomStyleSheets ()Ljava/util/List;
public final fun getFooterMessage ()Ljava/lang/String;
public final fun getMergeImplicitExpectActualDeclarations ()Z
public final fun getSeparateInheritedMembers ()Z
+ public final fun getTemplatesDir ()Ljava/io/File;
public fun hashCode ()I
public final fun setCustomAssets (Ljava/util/List;)V
public final fun setCustomStyleSheets (Ljava/util/List;)V
public final fun setFooterMessage (Ljava/lang/String;)V
public final fun setMergeImplicitExpectActualDeclarations (Z)V
public final fun setSeparateInheritedMembers (Z)V
+ public final fun setTemplatesDir (Ljava/io/File;)V
public fun toString ()Ljava/lang/String;
}
@@ -99,6 +102,7 @@ public final class org/jetbrains/dokka/base/DokkaBaseConfiguration$Companion {
public final fun getDefaultCustomAssets ()Ljava/util/List;
public final fun getDefaultCustomStyleSheets ()Ljava/util/List;
public final fun getDefaultFooterMessage ()Ljava/lang/String;
+ public final fun getDefaultTemplatesDir ()Ljava/io/File;
}
public final class org/jetbrains/dokka/base/generation/SingleModuleGeneration : org/jetbrains/dokka/generation/Generation {
@@ -216,6 +220,7 @@ public final class org/jetbrains/dokka/base/parsers/moduleAndPackage/ParseModule
}
public final class org/jetbrains/dokka/base/renderers/ContentTypeCheckingKt {
+ public static final fun getURIExtension (Ljava/lang/String;)Ljava/lang/String;
public static final fun isImage (Ljava/lang/String;)Z
public static final fun isImage (Lorg/jetbrains/dokka/pages/ContentEmbeddedResource;)Z
}
@@ -526,6 +531,7 @@ public final class org/jetbrains/dokka/base/renderers/html/TagsKt {
public static final fun templateCommand (Lkotlinx/html/TagConsumer;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun templateCommand$default (Lkotlinx/html/FlowOrMetaDataContent;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun templateCommand$default (Lkotlinx/html/TagConsumer;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
+ public static final fun templateCommandAsHtmlComment (Ljava/lang/Appendable;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;)V
public static final fun templateCommandAsHtmlComment (Lkotlinx/html/FlowOrMetaDataContent;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun templateCommandAsHtmlComment$default (Lkotlinx/html/FlowOrMetaDataContent;Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun templateCommandFor (Lorg/jetbrains/dokka/base/templating/Command;Lkotlinx/html/TagConsumer;)Lorg/jetbrains/dokka/base/renderers/html/TemplateCommand;
@@ -578,6 +584,55 @@ public final class org/jetbrains/dokka/base/renderers/html/command/consumers/Res
public fun processCommandAndFinalize (Lorg/jetbrains/dokka/base/templating/Command;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/dokka/base/renderers/html/command/consumers/ImmediateResolutionTagConsumer;)Ljava/lang/Object;
}
+public final class org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory : org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelFactory {
+ public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V
+ public fun buildModel (Lorg/jetbrains/dokka/pages/PageNode;Ljava/util/List;Lorg/jetbrains/dokka/base/resolvers/local/LocationProvider;ZLjava/lang/String;)Ljava/util/Map;
+ public fun buildSharedModel ()Ljava/util/Map;
+ public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext;
+}
+
+public final class org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory$SourceSetModel {
+ public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ public final fun component1 ()Ljava/lang/String;
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Ljava/lang/String;
+ public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory$SourceSetModel;
+ public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory$SourceSetModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelFactory$SourceSetModel;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getFilter ()Ljava/lang/String;
+ public final fun getName ()Ljava/lang/String;
+ public final fun getPlatform ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class org/jetbrains/dokka/base/renderers/html/innerTemplating/DefaultTemplateModelMerger : org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelMerger {
+ public fun <init> ()V
+ public fun invoke (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/util/Map;
+}
+
+public final class org/jetbrains/dokka/base/renderers/html/innerTemplating/DokkaTemplateTypes : java/lang/Enum {
+ public static final field BASE Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DokkaTemplateTypes;
+ public final fun getPath ()Ljava/lang/String;
+ public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DokkaTemplateTypes;
+ public static fun values ()[Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DokkaTemplateTypes;
+}
+
+public final class org/jetbrains/dokka/base/renderers/html/innerTemplating/HtmlTemplater {
+ public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V
+ public final fun renderFromTemplate (Lorg/jetbrains/dokka/base/renderers/html/innerTemplating/DokkaTemplateTypes;Lkotlin/jvm/functions/Function0;)Ljava/lang/String;
+ public final fun setupSharedModel (Ljava/util/Map;)V
+}
+
+public abstract interface class org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelFactory {
+ public abstract fun buildModel (Lorg/jetbrains/dokka/pages/PageNode;Ljava/util/List;Lorg/jetbrains/dokka/base/resolvers/local/LocationProvider;ZLjava/lang/String;)Ljava/util/Map;
+ public abstract fun buildSharedModel ()Ljava/util/Map;
+}
+
+public abstract interface class org/jetbrains/dokka/base/renderers/html/innerTemplating/TemplateModelMerger {
+ public abstract fun invoke (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/util/Map;
+}
+
public final class org/jetbrains/dokka/base/resolvers/anchors/SymbolAnchorHint : org/jetbrains/dokka/model/properties/ExtraProperty {
public static final field Companion Lorg/jetbrains/dokka/base/resolvers/anchors/SymbolAnchorHint$Companion;
public fun <init> (Ljava/lang/String;Lorg/jetbrains/dokka/pages/Kind;)V
diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts
index c9f57df8..e77d271e 100644
--- a/plugins/base/build.gradle.kts
+++ b/plugins/base/build.gradle.kts
@@ -11,6 +11,10 @@ dependencies {
val jackson_version: String by project
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
+
+ val freemarker_version: String by project
+ implementation("org.freemarker:freemarker:$freemarker_version")
+
testImplementation(project(":plugins:base:base-test-utils"))
testImplementation(project(":core:content-matcher-test-utils"))
diff --git a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt
index 8ea8818d..a9ccf600 100644
--- a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt
+++ b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt
@@ -9,7 +9,8 @@ data class DokkaBaseConfiguration(
var customAssets: List<File> = defaultCustomAssets,
var separateInheritedMembers: Boolean = separateInheritedMembersDefault,
var footerMessage: String = defaultFooterMessage,
- var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault
+ var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault,
+ var templatesDir: File? = defaultTemplatesDir
) : ConfigurableBlock {
companion object {
val defaultFooterMessage = "© ${Year.now().value} Copyright"
@@ -17,5 +18,6 @@ data class DokkaBaseConfiguration(
val defaultCustomAssets: List<File> = emptyList()
const val separateInheritedMembersDefault: Boolean = false
const val mergeImplicitExpectActualDeclarationsDefault: Boolean = false
+ val defaultTemplatesDir: File? = null
}
} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt b/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt
index 4619bc53..1cec4769 100644
--- a/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt
+++ b/plugins/base/src/main/kotlin/renderers/contentTypeChecking.kt
@@ -8,8 +8,11 @@ fun ContentEmbeddedResource.isImage(): Boolean {
return File(address).extension.toLowerCase() in imageExtensions
}
+val String.URIExtension: String
+ get() = substringBefore('?').substringAfterLast('.')
+
fun String.isImage(): Boolean =
- substringBefore('?').substringAfterLast('.') in imageExtensions
+ URIExtension in imageExtensions
object HtmlFileExtensions {
val imageExtensions = setOf("png", "jpg", "jpeg", "gif", "bmp", "tif", "webp", "svg")
diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
index 0ba085cd..05559469 100644
--- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt
@@ -4,10 +4,12 @@ import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.DokkaSourceSetID
import org.jetbrains.dokka.base.DokkaBase
-import org.jetbrains.dokka.base.DokkaBaseConfiguration
-import org.jetbrains.dokka.base.DokkaBaseConfiguration.Companion.defaultFooterMessage
import org.jetbrains.dokka.base.renderers.*
import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelFactory
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelMerger
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.DokkaTemplateTypes
+import org.jetbrains.dokka.base.renderers.html.innerTemplating.HtmlTemplater
import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint
import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider
import org.jetbrains.dokka.base.templating.*
@@ -28,8 +30,6 @@ internal const val TEMPLATE_REPLACEMENT: String = "###"
open class HtmlRenderer(
context: DokkaContext
) : DefaultRenderer<FlowContent>(context) {
- private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context)
-
private val sourceSetDependencyMap: Map<DokkaSourceSetID, List<DokkaSourceSetID>> =
context.configuration.sourceSets.associate { sourceSet ->
sourceSet.sourceSetID to context.configuration.sourceSets
@@ -37,6 +37,12 @@ open class HtmlRenderer(
.filter { it in sourceSet.dependentSourceSets }
}
+ private val templateModelFactories = listOf(DefaultTemplateModelFactory(context)) // TODO: Make extension point
+ private val templateModelMerger = DefaultTemplateModelMerger()
+ private val templater = HtmlTemplater(context).apply {
+ setupSharedModel(templateModelMerger.invoke(templateModelFactories) { buildSharedModel() })
+ }
+
private var shouldRenderSourceSetBubbles: Boolean = false
override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors }
@@ -764,138 +770,34 @@ open class HtmlRenderer(
override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String =
buildHtml(page, page.embeddedResources) {
- div("main-content") {
- id = "content"
- attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}"
- content(this, page)
- }
+ content(this, page)
}
private val String.isAbsolute: Boolean
get() = URI(this).isAbsolute
- open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit): String {
- val path = locationProvider.resolve(page)
- val pathToRoot = locationProvider.pathToRoot(page)
- return createHTML().prepareForTemplates().html {
- head {
- meta(name = "viewport", content = "width=device-width, initial-scale=1", charset = "UTF-8")
- title(page.name)
- templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- link(href = "${TEMPLATE_REPLACEMENT}images/logo-icon.svg", rel = "icon", type = "image/svg")
- }
- templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- script { unsafe { +"""var pathToRoot = "$TEMPLATE_REPLACEMENT";""" } }
- }
- // This script doesn't need to be there but it is nice to have since app in dark mode doesn't 'blink' (class is added before it is rendered)
- script {
- unsafe {
- +"""
- const storage = localStorage.getItem("dokka-dark-mode")
- const savedDarkMode = storage ? JSON.parse(storage) : false
- if(savedDarkMode === true){
- document.getElementsByTagName("html")[0].classList.add("theme-dark")
- }
- """.trimIndent()
- }
- }
- resources.forEach {
- when {
- it.substringBefore('?').substringAfterLast('.') == "css" ->
- if (it.isAbsolute) link(
- rel = LinkRel.stylesheet,
- href = it
- )
- else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- link(
- rel = LinkRel.stylesheet,
- href = TEMPLATE_REPLACEMENT + it
- )
- }
- it.substringBefore('?').substringAfterLast('.') == "js" ->
- if (it.isAbsolute) script(
- type = ScriptType.textJavaScript,
- src = it
- ) {
- async = true
- } else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- script(
- type = ScriptType.textJavaScript,
- src = TEMPLATE_REPLACEMENT + it
- ) {
- if (it == "scripts/main.js")
- defer = true
- else
- async = true
- }
- }
- it.isImage() -> if (it.isAbsolute) link(href = it)
- else templateCommandAsHtmlComment(PathToRootSubstitutionCommand(TEMPLATE_REPLACEMENT, default = pathToRoot)) {
- link(href = TEMPLATE_REPLACEMENT + it)
- }
- else -> unsafe { +it }
- }
- }
- }
- body {
- div("navigation-wrapper") {
- id = "navigation-wrapper"
- div {
- id = "leftToggler"
- span("icon-toggler")
- }
- div("library-name") {
- clickableLogo(page, pathToRoot)
- }
- div { templateCommand(ReplaceVersionsCommand(path.orEmpty())) }
- div("pull-right d-flex") {
- filterButtons(page)
- button {
- id = "theme-toggle-button"
- span {
- id = "theme-toggle"
- }
- }
- div {
- id = "searchBar"
- }
- }
- }
- div {
- id = "container"
- div {
- id = "leftColumn"
- div {
- id = "sideMenu"
- }
- }
- div {
- id = "main"
- content()
- div(classes = "footer") {
- span("go-to-top-icon") {
- a(href = "#content") {
- id = "go-to-top-link"
- }
- }
- span {
- configuration?.footerMessage?.takeIf { it.isNotEmpty() }
- ?.let { unsafe { raw(it) } }
- ?: text(defaultFooterMessage)
- }
- span("pull-right") {
- span { text("Generated by ") }
- a(href = "https://github.com/Kotlin/dokka") {
- span { text("dokka") }
- span(classes = "padded-icon")
- }
- }
- }
+
+ open fun buildHtml(page: PageNode, resources: List<String>, content: FlowContent.() -> Unit): String =
+ templater.renderFromTemplate(DokkaTemplateTypes.BASE) {
+ val generatedContent =
+ createHTML().div("main-content") {
+ id = "content"
+ (page as? ContentPage)?.let {
+ attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}"
}
+ content()
}
+
+ templateModelMerger.invoke(templateModelFactories) {
+ buildModel(
+ page,
+ resources,
+ locationProvider,
+ shouldRenderSourceSetBubbles,
+ generatedContent
+ )
}
}
- }
/**
* This is deliberately left open for plugins that have some other pages above ours and would like to link to them
diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt
index 94a53c27..ef27b934 100644
--- a/plugins/base/src/main/kotlin/renderers/html/Tags.kt
+++ b/plugins/base/src/main/kotlin/renderers/html/Tags.kt
@@ -38,6 +38,12 @@ fun FlowOrMetaDataContent.templateCommandAsHtmlComment(data: Command, block: Flo
comment(TEMPLATE_COMMAND_END_BORDER)
}
+fun <T: Appendable> T.templateCommandAsHtmlComment(command: Command, action: T.() -> Unit ) {
+ append("<!--$TEMPLATE_COMMAND_BEGIN_BORDER$TEMPLATE_COMMAND_SEPARATOR${toJsonString(command)}-->")
+ action()
+ append("<!--$TEMPLATE_COMMAND_END_BORDER-->")
+}
+
fun FlowOrMetaDataContent.templateCommand(data: Command, block: TemplateBlock = {}): Unit =
(consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block)
?: TemplateCommand(attributesMapOf("data", toJsonString(data)), consumer).visit(block)
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt
new file mode 100644
index 00000000..9f1ca57e
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt
@@ -0,0 +1,207 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+import freemarker.core.Environment
+import freemarker.template.*
+import kotlinx.html.*
+import kotlinx.html.stream.createHTML
+import org.jetbrains.dokka.DokkaConfiguration
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import org.jetbrains.dokka.base.renderers.URIExtension
+import org.jetbrains.dokka.base.renderers.html.TEMPLATE_REPLACEMENT
+import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
+import org.jetbrains.dokka.base.renderers.html.templateCommand
+import org.jetbrains.dokka.base.renderers.html.templateCommandAsHtmlComment
+import org.jetbrains.dokka.base.renderers.isImage
+import org.jetbrains.dokka.base.resolvers.local.LocationProvider
+import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand
+import org.jetbrains.dokka.base.templating.ProjectNameSubstitutionCommand
+import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand
+import org.jetbrains.dokka.base.templating.SubstitutionCommand
+import org.jetbrains.dokka.model.DisplaySourceSet
+import org.jetbrains.dokka.model.withDescendants
+import org.jetbrains.dokka.pages.ContentPage
+import org.jetbrains.dokka.pages.PageNode
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+import java.net.URI
+
+class DefaultTemplateModelFactory(val context: DokkaContext) : TemplateModelFactory {
+ private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context)
+ private val isPartial = context.configuration.delayTemplateSubstitution
+
+ private fun <R> TagConsumer<R>.prepareForTemplates() =
+ if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this
+ else ImmediateResolutionTagConsumer(this, context)
+
+ data class SourceSetModel(val name: String, val platform: String, val filter: String)
+
+ override fun buildModel(
+ page: PageNode,
+ resources: List<String>,
+ locationProvider: LocationProvider,
+ shouldRenderSourceSetBubbles: Boolean,
+ content: String
+ ): TemplateMap {
+ val path = locationProvider.resolve(page)
+ val pathToRoot = locationProvider.pathToRoot(page)
+ val mapper = mutableMapOf<String, Any>()
+ mapper["pageName"] = page.name
+ mapper["resources"] = PrintDirective {
+ val sb = StringBuilder()
+ if (isPartial)
+ sb.templateCommandAsHtmlComment(
+ PathToRootSubstitutionCommand(
+ TEMPLATE_REPLACEMENT,
+ default = pathToRoot
+ )
+ ) { resourcesForPage(TEMPLATE_REPLACEMENT, resources) }
+ else
+ sb.resourcesForPage(pathToRoot, resources)
+ sb.toString()
+ }
+ mapper["content"] = PrintDirective { content }
+ mapper["version"] = PrintDirective {
+ createHTML().prepareForTemplates().templateCommand(ReplaceVersionsCommand(path.orEmpty()))
+ }
+ mapper["template_cmd"] = TemplateDirective(context.configuration, pathToRoot)
+
+ if (shouldRenderSourceSetBubbles && page is ContentPage) {
+ val sourceSets = page.content.withDescendants()
+ .flatMap { it.sourceSets }
+ .distinct()
+ .sortedBy { it.comparableKey }
+ .map { SourceSetModel(it.name, it.platform.key, it.sourceSetIDs.merged.toString()) }
+ .toList()
+ mapper["sourceSets"] = sourceSets
+ }
+ return mapper
+ }
+
+ override fun buildSharedModel(): TemplateMap = mapOf<String, Any>(
+ "footerMessage" to (configuration?.footerMessage?.takeIf { it.isNotEmpty() }
+ ?: DokkaBaseConfiguration.defaultFooterMessage)
+ )
+
+ private val DisplaySourceSet.comparableKey
+ get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName }
+ private val String.isAbsolute: Boolean
+ get() = URI(this).isAbsolute
+
+ private fun Appendable.resourcesForPage(pathToRoot: String, resources: List<String>): Unit =
+ resources.forEach {
+ append(with(createHTML()) {
+ when {
+ it.URIExtension == "css" ->
+ link(
+ rel = LinkRel.stylesheet,
+ href = if (it.isAbsolute) it else "$pathToRoot$it"
+ )
+ it.URIExtension == "js" ->
+ script(
+ type = ScriptType.textJavaScript,
+ src = if (it.isAbsolute) it else "$pathToRoot$it"
+ ) {
+ if (it == "scripts/main.js")
+ defer = true
+ else
+ async = true
+ }
+ it.isImage() -> link(href = if (it.isAbsolute) it else "$pathToRoot$it")
+ else -> null
+ }
+ } ?: it)
+ }
+}
+
+private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel {
+ override fun execute(
+ env: Environment,
+ params: MutableMap<Any?, Any?>?,
+ loopVars: Array<TemplateModel>?,
+ body: TemplateDirectiveBody?
+ ) {
+ if (params?.isNotEmpty() == true) throw TemplateModelException(
+ "Parameters are not allowed"
+ )
+ if (loopVars?.isNotEmpty() == true) throw TemplateModelException(
+ "Loop variables are not allowed"
+ )
+ env.out.write(generateData())
+ }
+}
+
+private class TemplateDirective(val configuration: DokkaConfiguration, val pathToRoot: String) : TemplateDirectiveModel {
+ override fun execute(
+ env: Environment,
+ params: MutableMap<Any?, Any?>?,
+ loopVars: Array<TemplateModel>?,
+ body: TemplateDirectiveBody?
+ ) {
+ val commandName = params?.get(PARAM_NAME) ?: throw TemplateModelException(
+ "The required $PARAM_NAME parameter is missing."
+ )
+ val replacement = (params[PARAM_REPLACEMENT] as? SimpleScalar)?.asString ?: TEMPLATE_REPLACEMENT
+
+ when ((commandName as? SimpleScalar)?.asString) {
+ "pathToRoot" -> {
+ body ?: throw TemplateModelException(
+ "No directive body for $commandName command."
+ )
+ executeSubstituteCommand(
+ PathToRootSubstitutionCommand(
+ replacement, pathToRoot
+ ),
+ "pathToRoot",
+ pathToRoot,
+ Context(env, body)
+ )
+ }
+ "projectName" -> {
+ body ?: throw TemplateModelException(
+ "No directive body $commandName command."
+ )
+ executeSubstituteCommand(
+ ProjectNameSubstitutionCommand(
+ replacement, configuration.moduleName
+ ),
+ "projectName",
+ configuration.moduleName,
+ Context(env, body)
+ )
+ }
+ else -> throw TemplateModelException(
+ "The parameter $PARAM_NAME $commandName is unknown"
+ )
+ }
+ }
+
+ private data class Context(val env: Environment, val body: TemplateDirectiveBody)
+
+ private fun executeSubstituteCommand(
+ command: SubstitutionCommand,
+ name: String,
+ value: String,
+ ctx: Context
+ ) {
+ if (configuration.delayTemplateSubstitution)
+ ctx.env.out.templateCommandAsHtmlComment(command) {
+ renderWithLocalVar(name, command.pattern, ctx)
+ }
+ else {
+ renderWithLocalVar(name, value, ctx)
+ }
+ }
+
+ private fun renderWithLocalVar(name: String, value: String, ctx: Context) =
+ with(ctx) {
+ env.setVariable(name, SimpleScalar(value))
+ body.render(env.out)
+ env.setVariable(name, null)
+ }
+
+ companion object {
+ const val PARAM_NAME = "name"
+ const val PARAM_REPLACEMENT = "replacement"
+ }
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt
new file mode 100644
index 00000000..7d548721
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt
@@ -0,0 +1,16 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+class DefaultTemplateModelMerger : TemplateModelMerger {
+ override fun invoke(
+ factories: List<TemplateModelFactory>,
+ buildModel: TemplateModelFactory.() -> TemplateMap
+ ): TemplateMap {
+ val mapper = mutableMapOf<String, Any?>()
+ factories.map(buildModel).forEach { partialModel ->
+ partialModel.forEach { (k, v) ->
+ mapper[k] = v
+ }
+ }
+ return mapper
+ }
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt
new file mode 100644
index 00000000..e3d16d98
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt
@@ -0,0 +1,76 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+import freemarker.cache.ClassTemplateLoader
+import freemarker.cache.FileTemplateLoader
+import freemarker.cache.MultiTemplateLoader
+import freemarker.log.Logger
+import freemarker.template.Configuration
+import freemarker.template.TemplateExceptionHandler
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.configuration
+import java.io.StringWriter
+
+
+enum class DokkaTemplateTypes(val path: String) {
+ BASE("base.ftl")
+}
+
+typealias TemplateMap = Map<String, Any?>
+
+class HtmlTemplater(
+ context: DokkaContext
+) {
+
+ init {
+ // to disable logging, but it isn't reliable see [Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY]
+ // (use SLF4j further)
+ System.setProperty(
+ Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY,
+ System.getProperty(Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY) ?: Logger.LIBRARY_NAME_NONE
+ )
+ }
+
+ private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context)
+ private val templaterConfiguration =
+ Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() }
+
+ private fun Configuration.configureTemplateEngine() {
+ val loaderFromResources = ClassTemplateLoader(javaClass, "/dokka/templates")
+ templateLoader = configuration?.templatesDir?.let {
+ MultiTemplateLoader(
+ arrayOf(
+ FileTemplateLoader(it),
+ loaderFromResources
+ )
+ )
+ } ?: loaderFromResources
+
+ unsetLocale()
+ defaultEncoding = "UTF-8"
+ templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER
+ logTemplateExceptions = false
+ wrapUncheckedExceptions = true
+ fallbackOnNullLoopVariable = false
+ templateUpdateDelayMilliseconds = Long.MAX_VALUE
+ }
+
+ fun setupSharedModel(model: TemplateMap) {
+ templaterConfiguration.setSharedVariables(model)
+ }
+
+ fun renderFromTemplate(
+ templateType: DokkaTemplateTypes,
+ generateModel: () -> TemplateMap
+ ): String {
+ val out = StringWriter()
+ // Freemarker has own thread-safe cache to keep templates
+ val template = templaterConfiguration.getTemplate(templateType.path)
+ val model = generateModel()
+ template.process(model, out)
+
+ return out.toString()
+ }
+}
+
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt
new file mode 100644
index 00000000..ceecf201
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt
@@ -0,0 +1,16 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+import org.jetbrains.dokka.base.resolvers.local.LocationProvider
+import org.jetbrains.dokka.pages.PageNode
+
+interface TemplateModelFactory {
+ fun buildModel(
+ page: PageNode,
+ resources: List<String>,
+ locationProvider: LocationProvider,
+ shouldRenderSourceSetBubbles: Boolean,
+ content: String
+ ): TemplateMap
+
+ fun buildSharedModel(): TemplateMap
+} \ No newline at end of file
diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt
new file mode 100644
index 00000000..7ad96d8f
--- /dev/null
+++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt
@@ -0,0 +1,5 @@
+package org.jetbrains.dokka.base.renderers.html.innerTemplating
+
+fun interface TemplateModelMerger {
+ fun invoke(factories: List<TemplateModelFactory>, buildModel: TemplateModelFactory.() -> TemplateMap): TemplateMap
+} \ No newline at end of file
diff --git a/plugins/base/src/main/resources/dokka/templates/base.ftl b/plugins/base/src/main/resources/dokka/templates/base.ftl
new file mode 100644
index 00000000..853d1ca2
--- /dev/null
+++ b/plugins/base/src/main/resources/dokka/templates/base.ftl
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta name="viewport" content="width=device-width, initial-scale=1" charset="UTF-8">
+ <title>${pageName}</title>
+ <@template_cmd name="pathToRoot">
+ <link href="${pathToRoot}images/logo-icon.svg" rel="icon" type="image/svg">
+ <script>var pathToRoot = "${pathToRoot}";</script>
+ </@template_cmd>
+ <#-- This script doesn't need to be there but it is nice to have
+ since app in dark mode doesn't 'blink' (class is added before it is rendered) -->
+ <script>const storage = localStorage.getItem("dokka-dark-mode")
+const savedDarkMode = storage ? JSON.parse(storage) : false
+if(savedDarkMode === true){
+ document.getElementsByTagName("html")[0].classList.add("theme-dark")
+}</script>
+ <#-- Resources (scripts, stylesheets) are handled by Dokka.
+ Use customStyleSheets and customAssets to change them. -->
+ <@resources/>
+</head>
+<body>
+<div class="navigation-wrapper" id="navigation-wrapper">
+ <div id="leftToggler"><span class="icon-toggler"></span></div>
+ <div class="library-name">
+ <@template_cmd name="pathToRoot">
+ <a href="${pathToRoot}index.html">
+ <@template_cmd name="projectName">
+ <span>${projectName}</span>
+ </@template_cmd>
+ </a>
+ </@template_cmd>
+ </div>
+ <div>
+ <#-- This can be handled by a versioning plugin -->
+ <@version/>
+ </div>
+ <div class="pull-right d-flex">
+ <button id="theme-toggle-button"><span id="theme-toggle"></span></button>
+ <div id="searchBar"></div>
+ <#if sourceSets??>
+ <div class="filter-section" id="filter-section">
+ <#list sourceSets as ss>
+ <button class="platform-tag platform-selector ${ss.platform}-like" data-active="" data-filter="${ss.filter}">${ss.name}</button>
+ </#list>
+ </div>
+ </#if>
+ </div>
+</div>
+<div id="container">
+ <div id="leftColumn">
+ <div id="sideMenu"></div>
+ </div>
+ <div id="main">
+ <@content/>
+ <div class="footer"><span class="go-to-top-icon"><a href="#content" id="go-to-top-link"></a></span><span>${footerMessage}</span><span
+ class="pull-right"><span>Generated by </span><a
+ href="https://github.com/Kotlin/dokka"><span>dokka</span><span class="padded-icon"></span></a></span>
+ </div>
+ </div>
+</div>
+</body>
+</html> \ No newline at end of file