aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/src/doc/docs/user_guide/base-specific/frontend.md41
-rw-r--r--gradle.properties1
-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
14 files changed, 524 insertions, 138 deletions
diff --git a/docs/src/doc/docs/user_guide/base-specific/frontend.md b/docs/src/doc/docs/user_guide/base-specific/frontend.md
index e6802639..ecbd6964 100644
--- a/docs/src/doc/docs/user_guide/base-specific/frontend.md
+++ b/docs/src/doc/docs/user_guide/base-specific/frontend.md
@@ -2,12 +2,12 @@
## Prerequisites
-Dokka's Html format requires a web server to view documentation correctly.
+Dokka's HTML format requires a web server to view documentation correctly.
This can be achieved by using the one that is build in IntelliJ or providing your own.
If this requisite is not fulfilled Dokka with fail to load navigation pane and search bars.
!!! important
- Concepts specified below apply only to configuration of the Base Plugin (that contains Html format)
+ Concepts specified below apply only to configuration of the Base Plugin (that contains HTML format)
and needs to be applied via pluginsConfiguration and not on the root one.
## Modifying assets
@@ -20,19 +20,21 @@ Currently, user can modify:
Every file provided in those values will be applied to **every** page.
-Dokka uses 3 stylesheets:
+Dokka uses 4 stylesheets:
* `style.css` - main css file responsible for styling the page
* `jetbrains-mono.css` - fonts used across dokka
* `logo-styles.css` - logo styling
+* [`prism.css`](https://github.com/Kotlin/dokka/blob/master/plugins/base/src/main/resources/dokka/styles/prism.css) - code highlighting
-User can choose to add or override those files.
+Also, it uses js scripts. The actual ones are [here](https://github.com/Kotlin/dokka/tree/master/plugins/base/src/main/resources/dokka/scripts).
+User can choose to add or override those files - stylesheets and js scripts.
Resources will be overridden when in `pluginConfiguration` block there is a resource with the same name.
## Modifying footer
Dokka supports custom messages in the footer via `footerMessage` string property on base plugin configuration.
-Keep in mind that this value will be passed exactly to the output html, so it has to be valid and escaped correctly.
+Keep in mind that this value will be passed exactly to the output HTML, so it has to be valid and escaped correctly.
## Separating inherited members
@@ -69,3 +71,32 @@ In order to override a logo and style it accordingly a css file named `logo-styl
For build system specific instructions please visit dedicated pages: [gradle](../gradle/usage.md#applying-plugins), [maven](../maven/usage.md#applying-plugins) and [cli](../cli/usage.md#configuration-options)
+
+## Custom HTML pages
+
+Templates are taken from the folder that is defined by the `templatesDir` property.
+To customize HTML output, you can use the [default template](https://github.com/Kotlin/dokka/blob/master/plugins/base/src/main/resources/dokka/templates) as a starting point.
+
+!!! note
+ To change page assets, you can set properties `customAssets` and `customStyleSheets`.
+ Assets are handled by Dokka itself, not FreeMaker.
+
+Currently, there is only one template file with predefined name `base.ftl`. It defines general design of all pages to render.
+If `templatesDir` is defined, Dokka will find the `base.ftl` file there.
+
+Variables given below are available to the template:
+ - `${pageName}` - the page name
+ - `${footerMessage}` - text that is set by the `footerMessage` property
+ - `${sourceSets}` - a nullable list of source sets, only for multi-platform pages. Each source set has `name`, `platfrom` and `filter` properties.
+
+Also, Dokka-defined [directives](https://freemarker.apache.org/docs/ref_directive_userDefined.html) can be used:
+ - `<@content/>` - main content
+ - `<@resources/>` - scripts, stylesheets
+ - `<@version/>` - version ([versioning-plugin](https://kotlin.github.io/dokka/1.6.10/user_guide/versioning/versioning/) will replace this with a version navigator)
+ - `<@template_cmd name="...""> ...</@template_cmd>` - is used for variables that depend on the root project (such `pathToRoot`, `projectName`). They are available only inside the directive. This is processed by a multi-module task that assembles partial outputs from modules.
+ Example:
+ ```
+ <@template_cmd name="projectName">
+ <span>${projectName}</span>
+ </@template_cmd>
+ ``` \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index ee7f80f6..1474d5c8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,6 +11,7 @@ jsoup_version=1.13.1
idea_version=211.7442.40
language_version=1.4
jackson_version=2.12.4
+freemarker_version=2.3.31
# Code style
kotlin.code.style=official
# Gradle settings
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