From 29065cebd9337f172b75efe1373793f807599ff2 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Fri, 18 Feb 2022 16:47:18 +0300 Subject: [PATCH 01/10] Add customization option --- .../docs/user_guide/base-specific/frontend.md | 33 ++- gradle.properties | 1 + plugins/base/api/base.api | 81 ++++++- plugins/base/build.gradle.kts | 4 + .../src/main/kotlin/DokkaBaseConfiguration.kt | 4 +- .../kotlin/renderers/contentTypeChecking.kt | 5 +- .../kotlin/renderers/html/HtmlRenderer.kt | 146 ++----------- .../kotlin/renderers/html/HtmlTemplater.kt | 78 +++++++ .../src/main/kotlin/renderers/html/Tags.kt | 6 + .../renderers/html/TemplateModelFactory.kt | 205 ++++++++++++++++++ .../main/resources/dokka/templates/base.ftl | 62 ++++++ 11 files changed, 490 insertions(+), 135 deletions(-) create mode 100644 plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt create mode 100644 plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt create mode 100644 plugins/base/src/main/resources/dokka/templates/base.ftl 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 e6802639dd..f016587750 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 +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. 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,26 @@ 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 + +Dokka uses [FreeMarker](https://freemarker.apache.org/) template engine to render pages. +It takes templates from a folder that is set by a property `templatesDir`. +To custom HTML output user can use a [default template](https://github.com/Kotlin/dokka/blob/master/plugins/base/src/main/resources/dokka/templates) as a basic. + +!!! note + To change page assets user can set properties `customAssets` and `customStyleSheets`. + Assets are handled by Dokka. + +Currently, there is one template file `base.ftl`. It defines general design of all pages to render. + +Variables given below are available to a template: + - `${pageName}` - a page name + - `${footerMessage}` - a text is set by `footerMessage` property + - `${sourceSets}` - a nullable list of source set, 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/>` - a main content + - `<@resources/>` - scripts, stylesheets + - `<@version/>` - version (A version plugin replace this with a version navigator) + - `<@template_cmd name="...""> ...` - is used for stuff (`pathToRoot`, `projectName` are `name` parameter, local variables as well) that dependent on a root project. This is processed by a multi-module task that assembles a partial outputs from modules. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index dfc59909dd..37c018e631 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 a38c2cfd8f..530424bf6a 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 ()V - public fun (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;Z)V - public synthetic fun (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;)V + public synthetic fun (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 } @@ -330,6 +335,13 @@ public final class org/jetbrains/dokka/base/renderers/html/CustomResourceInstall public fun invoke (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/pages/RootPageNode; } +public final class org/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes : java/lang/Enum { + public static final field BASE Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes; + public final fun getPath ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes; + public static fun values ()[Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes; +} + public final class org/jetbrains/dokka/base/renderers/html/HtmlFormatingUtilsKt { public static final fun buildBreakableDotSeparatedHtml (Lkotlinx/html/FlowContent;Ljava/lang/String;)V public static final fun buildBreakableText (Lkotlinx/html/FlowContent;Ljava/lang/String;)V @@ -388,6 +400,12 @@ public final class org/jetbrains/dokka/base/renderers/html/HtmlRendererKt { public static final fun joinAttr (Ljava/util/List;)Ljava/lang/String; } +public final class org/jetbrains/dokka/base/renderers/html/HtmlTemplater { + public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public final fun renderFromTemplate (Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes;Lkotlin/jvm/functions/Function0;)Ljava/lang/String; + public final fun setupSharedModel (Ljava/util/Map;)V +} + public abstract class org/jetbrains/dokka/base/renderers/html/NavigationDataProvider { public fun ()V public fun navigableChildren (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/base/renderers/html/NavigationNode; @@ -526,6 +544,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; @@ -537,6 +556,60 @@ public final class org/jetbrains/dokka/base/renderers/html/TemplateCommand : kot public fun (Ljava/util/Map;Lkotlinx/html/TagConsumer;)V } +public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory { + public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public final fun buildModel (Lorg/jetbrains/dokka/pages/PageNode;Ljava/util/List;Lorg/jetbrains/dokka/base/resolvers/local/LocationProvider;ZLjava/lang/String;)Ljava/util/Map; + public final fun buildSharedModel ()Ljava/util/Map; + public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; +} + +public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$PrintDirective : freemarker/template/TemplateDirectiveModel { + public fun (Lkotlin/jvm/functions/Function0;)V + public fun execute (Lfreemarker/core/Environment;Ljava/util/Map;[Lfreemarker/template/TemplateModel;Lfreemarker/template/TemplateDirectiveBody;)V + public final fun getGenerateData ()Lkotlin/jvm/functions/Function0; +} + +public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$SourceSetModel { + public fun (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/TemplateModelFactory$SourceSetModel; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$SourceSetModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$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/TemplateModelFactory$TemplateDirective : freemarker/template/TemplateDirectiveModel { + public static final field Companion Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Companion; + public static final field PARAM_NAME Ljava/lang/String; + public static final field PARAM_REPLACEMENT Ljava/lang/String; + public fun (Lorg/jetbrains/dokka/DokkaConfiguration;Ljava/lang/String;)V + public fun execute (Lfreemarker/core/Environment;Ljava/util/Map;[Lfreemarker/template/TemplateModel;Lfreemarker/template/TemplateDirectiveBody;)V + public final fun getConfiguration ()Lorg/jetbrains/dokka/DokkaConfiguration; + public final fun getPathToRoot ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Companion { +} + +public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context { + public fun (Lfreemarker/core/Environment;Lfreemarker/template/TemplateDirectiveBody;)V + public final fun component1 ()Lfreemarker/core/Environment; + public final fun component2 ()Lfreemarker/template/TemplateDirectiveBody; + public final fun copy (Lfreemarker/core/Environment;Lfreemarker/template/TemplateDirectiveBody;)Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context;Lfreemarker/core/Environment;Lfreemarker/template/TemplateDirectiveBody;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context; + public fun equals (Ljava/lang/Object;)Z + public final fun getBody ()Lfreemarker/template/TemplateDirectiveBody; + public final fun getEnv ()Lfreemarker/core/Environment; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class org/jetbrains/dokka/base/renderers/html/WBR : kotlinx/html/HTMLTag, kotlinx/html/HtmlBlockInlineTag { public fun (Ljava/util/Map;Lkotlinx/html/TagConsumer;)V } diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index c9f57df86a..e77d271ecf 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 8ea8818ded..a9ccf600e7 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 = 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 = 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 4619bc53ea..1cec476934 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 0ba085cd1f..4bb7fddf78 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -5,7 +5,6 @@ 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.resolvers.anchors.SymbolAnchorHint @@ -764,138 +763,35 @@ 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, 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") - } - } - } + + private val templateModelFactory = TemplateModelFactory(context) + private val templater = HtmlTemplater(context).apply { setupSharedModel(templateModelFactory.buildSharedModel()) } + + open fun buildHtml(page: PageNode, resources: List, 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() } - } + + templateModelFactory.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/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt new file mode 100644 index 0000000000..e25b949935 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt @@ -0,0 +1,78 @@ +package org.jetbrains.dokka.base.renderers.html + +import freemarker.cache.ClassTemplateLoader +import freemarker.cache.FileTemplateLoader +import freemarker.cache.MultiTemplateLoader +import freemarker.template.Configuration +import freemarker.template.Template +import freemarker.template.TemplateExceptionHandler +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +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 + +class HtmlTemplater( + context: DokkaContext +) { + private val configuration = configuration(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 + } + + fun setupSharedModel(model: TemplateMap) { + templaterConfiguration.setSharedVariables(model) + } + + private val cachedTemplates: MutableMap = mutableMapOf() + + fun renderFromTemplate( + templateType: DokkaTemplateTypes, + generateModel: () -> TemplateMap + ): String { + val cachedTemplate = cachedTemplates[templateType] + val out = StringWriter() + if (cachedTemplate == null) { + runBlocking { + val templateDeferred = async { templaterConfiguration.getTemplate(templateType.path) } + val model = generateModel() + val template = templateDeferred.await() + cachedTemplates[templateType] = template + template.process(model, out) + + } + } else { + val model = generateModel() + cachedTemplate.process(model, out) + } + return out.toString() + } +} + diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt index 94a53c27fb..ef27b934b8 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.templateCommandAsHtmlComment(command: Command, action: T.() -> Unit ) { + append("") + action() + append("") +} + 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/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt new file mode 100644 index 0000000000..b804e378e4 --- /dev/null +++ b/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt @@ -0,0 +1,205 @@ +package org.jetbrains.dokka.base.renderers.html + +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.command.consumers.ImmediateResolutionTagConsumer +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 TemplateModelFactory(val context: DokkaContext) { + private val configuration = configuration(context) + private val isPartial = context.configuration.delayTemplateSubstitution + + private fun TagConsumer.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) + + fun buildModel( + page: PageNode, + resources: List, + locationProvider: LocationProvider, + shouldRenderSourceSetBubbles: Boolean, + content: String + ): TemplateMap { + val path = locationProvider.resolve(page) + val pathToRoot = locationProvider.pathToRoot(page) + val mapper = mutableMapOf() + mapper.put("pageName", page.name) + mapper.put("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.put("content", PrintDirective { content }) + mapper.put( + "version", + PrintDirective { + createHTML().prepareForTemplates().templateCommand(ReplaceVersionsCommand(path.orEmpty())) + } + ) + mapper.put("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 + } + + fun buildSharedModel(): TemplateMap = mapOf( + "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): 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) + } + + class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { + override fun execute( + env: Environment, + params: MutableMap?, + loopVars: Array?, + body: TemplateDirectiveBody? + ) { + if ((params?.size ?: 0) > 0) throw TemplateModelException( + "A parameter is not allowed" + ) + if ((loopVars?.size ?: 0) > 0) throw TemplateModelException( + "A loop variable is not allowed" + ) + env.out.write(generateData()) + } + } + + class TemplateDirective(val configuration: DokkaConfiguration, val pathToRoot: String) : TemplateDirectiveModel { + override fun execute( + env: Environment, + params: MutableMap?, + loopVars: Array?, + 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" + ) + } + } + + 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/resources/dokka/templates/base.ftl b/plugins/base/src/main/resources/dokka/templates/base.ftl new file mode 100644 index 0000000000..853d1ca2cf --- /dev/null +++ b/plugins/base/src/main/resources/dokka/templates/base.ftl @@ -0,0 +1,62 @@ + + + + + ${pageName} + <@template_cmd name="pathToRoot"> + + + + <#-- 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) --> + + <#-- Resources (scripts, stylesheets) are handled by Dokka. + Use customStyleSheets and customAssets to change them. --> + <@resources/> + + + +
+
+
+
+
+ <@content/> + +
+
+ + \ No newline at end of file From 1864b01480a8a030f49ada1c1d7e214de68b4b8d Mon Sep 17 00:00:00 2001 From: vmishenev Date: Tue, 22 Feb 2022 20:18:20 +0300 Subject: [PATCH 02/10] Disable FreeMarker Logger --- .../src/main/kotlin/renderers/html/HtmlTemplater.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt index e25b949935..775a3d38ef 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt @@ -3,6 +3,7 @@ package org.jetbrains.dokka.base.renderers.html import freemarker.cache.ClassTemplateLoader import freemarker.cache.FileTemplateLoader import freemarker.cache.MultiTemplateLoader +import freemarker.log.Logger import freemarker.template.Configuration import freemarker.template.Template import freemarker.template.TemplateExceptionHandler @@ -24,6 +25,16 @@ typealias TemplateMap = Map 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(context) private val templaterConfiguration = Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() } From 899e49d7c0dfcc131980f7cff836c1b4b63465bf Mon Sep 17 00:00:00 2001 From: vmishenev Date: Tue, 22 Feb 2022 20:22:24 +0300 Subject: [PATCH 03/10] Add example in documentation --- docs/src/doc/docs/user_guide/base-specific/frontend.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 f016587750..7d17ff6f8e 100644 --- a/docs/src/doc/docs/user_guide/base-specific/frontend.md +++ b/docs/src/doc/docs/user_guide/base-specific/frontend.md @@ -93,4 +93,10 @@ Also, Dokka-defined [directives](https://freemarker.apache.org/docs/ref_directiv - `<@content/>` - a main content - `<@resources/>` - scripts, stylesheets - `<@version/>` - version (A version plugin replace this with a version navigator) - - `<@template_cmd name="...""> ...` - is used for stuff (`pathToRoot`, `projectName` are `name` parameter, local variables as well) that dependent on a root project. This is processed by a multi-module task that assembles a partial outputs from modules. \ No newline at end of file + - `<@template_cmd name="...""> ...` - is used for stuff (`pathToRoot`, `projectName` are `name` parameter, local variables as well) that dependent on a root project. This is processed by a multi-module task that assembles a partial outputs from modules. + Example: + ``` + <@template_cmd name="projectName"> + ${projectName} + + ``` \ No newline at end of file From b05de3aa0c489991943a26ae0f84cf6a74724864 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Sat, 26 Feb 2022 02:08:29 +0300 Subject: [PATCH 04/10] Change doc --- .../docs/user_guide/base-specific/frontend.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 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 7d17ff6f8e..5a5caa74af 100644 --- a/docs/src/doc/docs/user_guide/base-specific/frontend.md +++ b/docs/src/doc/docs/user_guide/base-specific/frontend.md @@ -28,7 +28,7 @@ Dokka uses 4 stylesheets: * [`prism.css`](https://github.com/Kotlin/dokka/blob/master/plugins/base/src/main/resources/dokka/styles/prism.css) - code highlighting 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. +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 @@ -74,26 +74,26 @@ For build system specific instructions please visit dedicated pages: [gradle](.. ## Custom HTML pages -Dokka uses [FreeMarker](https://freemarker.apache.org/) template engine to render pages. -It takes templates from a folder that is set by a property `templatesDir`. -To custom HTML output user can use a [default template](https://github.com/Kotlin/dokka/blob/master/plugins/base/src/main/resources/dokka/templates) as a basic. +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 user can set properties `customAssets` and `customStyleSheets`. - Assets are handled by Dokka. + To change page assets, you can set properties `customAssets` and `customStyleSheets`. + Assets are handled by Dokka itself, not FreeMaker. -Currently, there is one template file `base.ftl`. It defines general design of all pages to render. +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 a template: - - `${pageName}` - a page name - - `${footerMessage}` - a text is set by `footerMessage` property - - `${sourceSets}` - a nullable list of source set, only for multi-platform pages. Each source set has `name`, `platfrom` and `filter` properties. +Variables given below are available to the template: + - `${pageName}` - the page name + - `${footerMessage}` - a 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/>` - a main content - `<@resources/>` - scripts, stylesheets - - `<@version/>` - version (A version plugin replace this with a version navigator) - - `<@template_cmd name="...""> ...` - is used for stuff (`pathToRoot`, `projectName` are `name` parameter, local variables as well) that dependent on a root project. This is processed by a multi-module task that assembles a partial outputs from modules. + - `<@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="...""> ...` - 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 a partial outputs from modules. Example: ``` <@template_cmd name="projectName"> From dbb68de0c6150a97486b4cfd9ff1bfdf98dd83cb Mon Sep 17 00:00:00 2001 From: vmishenev Date: Sat, 26 Feb 2022 03:00:07 +0300 Subject: [PATCH 05/10] Refactor --- plugins/base/api/base.api | 32 -------------- .../kotlin/renderers/html/HtmlRenderer.kt | 9 ++-- .../kotlin/renderers/html/HtmlTemplater.kt | 17 ++++---- .../renderers/html/TemplateModelFactory.kt | 43 +++++++++---------- 4 files changed, 33 insertions(+), 68 deletions(-) diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 530424bf6a..6c9f153952 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -563,12 +563,6 @@ public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; } -public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$PrintDirective : freemarker/template/TemplateDirectiveModel { - public fun (Lkotlin/jvm/functions/Function0;)V - public fun execute (Lfreemarker/core/Environment;Ljava/util/Map;[Lfreemarker/template/TemplateModel;Lfreemarker/template/TemplateDirectiveBody;)V - public final fun getGenerateData ()Lkotlin/jvm/functions/Function0; -} - public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$SourceSetModel { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; @@ -584,32 +578,6 @@ public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$ public fun toString ()Ljava/lang/String; } -public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective : freemarker/template/TemplateDirectiveModel { - public static final field Companion Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Companion; - public static final field PARAM_NAME Ljava/lang/String; - public static final field PARAM_REPLACEMENT Ljava/lang/String; - public fun (Lorg/jetbrains/dokka/DokkaConfiguration;Ljava/lang/String;)V - public fun execute (Lfreemarker/core/Environment;Ljava/util/Map;[Lfreemarker/template/TemplateModel;Lfreemarker/template/TemplateDirectiveBody;)V - public final fun getConfiguration ()Lorg/jetbrains/dokka/DokkaConfiguration; - public final fun getPathToRoot ()Ljava/lang/String; -} - -public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Companion { -} - -public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context { - public fun (Lfreemarker/core/Environment;Lfreemarker/template/TemplateDirectiveBody;)V - public final fun component1 ()Lfreemarker/core/Environment; - public final fun component2 ()Lfreemarker/template/TemplateDirectiveBody; - public final fun copy (Lfreemarker/core/Environment;Lfreemarker/template/TemplateDirectiveBody;)Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context; - public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context;Lfreemarker/core/Environment;Lfreemarker/template/TemplateDirectiveBody;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$TemplateDirective$Context; - public fun equals (Ljava/lang/Object;)Z - public final fun getBody ()Lfreemarker/template/TemplateDirectiveBody; - public final fun getEnv ()Lfreemarker/core/Environment; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - public class org/jetbrains/dokka/base/renderers/html/WBR : kotlinx/html/HTMLTag, kotlinx/html/HtmlBlockInlineTag { public fun (Ljava/util/Map;Lkotlinx/html/TagConsumer;)V } diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 4bb7fddf78..0a1d6bf052 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -4,7 +4,6 @@ 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.renderers.* import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint @@ -27,8 +26,6 @@ internal const val TEMPLATE_REPLACEMENT: String = "###" open class HtmlRenderer( context: DokkaContext ) : DefaultRenderer(context) { - private val configuration = configuration(context) - private val sourceSetDependencyMap: Map> = context.configuration.sourceSets.associate { sourceSet -> sourceSet.sourceSetID to context.configuration.sourceSets @@ -36,6 +33,9 @@ open class HtmlRenderer( .filter { it in sourceSet.dependentSourceSets } } + private val templateModelFactory = TemplateModelFactory(context) + private val templater = HtmlTemplater(context).apply { setupSharedModel(templateModelFactory.buildSharedModel()) } + private var shouldRenderSourceSetBubbles: Boolean = false override val preprocessors = context.plugin().query { htmlPreprocessors } @@ -770,9 +770,6 @@ open class HtmlRenderer( get() = URI(this).isAbsolute - private val templateModelFactory = TemplateModelFactory(context) - private val templater = HtmlTemplater(context).apply { setupSharedModel(templateModelFactory.buildSharedModel()) } - open fun buildHtml(page: PageNode, resources: List, content: FlowContent.() -> Unit): String = templater.renderFromTemplate(DokkaTemplateTypes.BASE) { val generatedContent = diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt index 775a3d38ef..abba93b533 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt @@ -5,7 +5,6 @@ import freemarker.cache.FileTemplateLoader import freemarker.cache.MultiTemplateLoader import freemarker.log.Logger import freemarker.template.Configuration -import freemarker.template.Template import freemarker.template.TemplateExceptionHandler import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking @@ -14,6 +13,7 @@ import org.jetbrains.dokka.base.DokkaBaseConfiguration import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.configuration import java.io.StringWriter +import java.util.concurrent.ConcurrentHashMap enum class DokkaTemplateTypes(val path: String) { @@ -38,6 +38,9 @@ class HtmlTemplater( private val configuration = configuration(context) private val templaterConfiguration = Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() } + private val cachedTemplates: MutableSet = + ConcurrentHashMap().keySet(true) + private fun Configuration.configureTemplateEngine() { val loaderFromResources = ClassTemplateLoader(javaClass, "/dokka/templates") @@ -62,26 +65,24 @@ class HtmlTemplater( templaterConfiguration.setSharedVariables(model) } - private val cachedTemplates: MutableMap = mutableMapOf() - fun renderFromTemplate( templateType: DokkaTemplateTypes, generateModel: () -> TemplateMap ): String { - val cachedTemplate = cachedTemplates[templateType] val out = StringWriter() - if (cachedTemplate == null) { + // Freemarker has own cache to keep templates + if (cachedTemplates.contains(templateType)) { // it's a heuristic, freemarker can remove a template from cache runBlocking { val templateDeferred = async { templaterConfiguration.getTemplate(templateType.path) } val model = generateModel() val template = templateDeferred.await() - cachedTemplates[templateType] = template + cachedTemplates.add(templateType) template.process(model, out) - } } else { + val template = templaterConfiguration.getTemplate(templateType.path) val model = generateModel() - cachedTemplate.process(model, out) + template.process(model, out) } return out.toString() } diff --git a/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt index b804e378e4..90b736407b 100644 --- a/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt +++ b/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt @@ -43,8 +43,8 @@ class TemplateModelFactory(val context: DokkaContext) { val path = locationProvider.resolve(page) val pathToRoot = locationProvider.pathToRoot(page) val mapper = mutableMapOf() - mapper.put("pageName", page.name) - mapper.put("resources", PrintDirective { + mapper["pageName"] = page.name + mapper["resources"] = PrintDirective { val sb = StringBuilder() if (isPartial) sb.templateCommandAsHtmlComment( @@ -56,21 +56,20 @@ class TemplateModelFactory(val context: DokkaContext) { else sb.resourcesForPage(pathToRoot, resources) sb.toString() - }) - mapper.put("content", PrintDirective { content }) - mapper.put( - "version", - PrintDirective { - createHTML().prepareForTemplates().templateCommand(ReplaceVersionsCommand(path.orEmpty())) - } - ) - mapper.put("template_cmd", TemplateDirective(context.configuration, pathToRoot)) + } + 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() + 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 @@ -111,24 +110,24 @@ class TemplateModelFactory(val context: DokkaContext) { } ?: it) } - class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { + private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { override fun execute( env: Environment, params: MutableMap?, loopVars: Array?, body: TemplateDirectiveBody? ) { - if ((params?.size ?: 0) > 0) throw TemplateModelException( - "A parameter is not allowed" + if (params?.isNotEmpty() == true) throw TemplateModelException( + "Parameters are not allowed" ) - if ((loopVars?.size ?: 0) > 0) throw TemplateModelException( - "A loop variable is not allowed" + if (loopVars?.isNotEmpty() == true) throw TemplateModelException( + "Loop variables are not allowed" ) env.out.write(generateData()) } } - class TemplateDirective(val configuration: DokkaConfiguration, val pathToRoot: String) : TemplateDirectiveModel { + private class TemplateDirective(val configuration: DokkaConfiguration, val pathToRoot: String) : TemplateDirectiveModel { override fun execute( env: Environment, params: MutableMap?, @@ -173,7 +172,7 @@ class TemplateModelFactory(val context: DokkaContext) { } } - data class Context(val env: Environment, val body: TemplateDirectiveBody) + private data class Context(val env: Environment, val body: TemplateDirectiveBody) private fun executeSubstituteCommand( command: SubstitutionCommand, From a4f22f903f826c698043a86e0064d39b9abc3a4c Mon Sep 17 00:00:00 2001 From: vmishenev Date: Sat, 26 Feb 2022 06:34:52 +0300 Subject: [PATCH 06/10] Set `TemplateUpdateDelay` --- plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt index abba93b533..1f6de97d53 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt @@ -59,6 +59,7 @@ class HtmlTemplater( logTemplateExceptions = false wrapUncheckedExceptions = true fallbackOnNullLoopVariable = false + templateUpdateDelayMilliseconds = Long.MAX_VALUE } fun setupSharedModel(model: TemplateMap) { From 5b6829f539385163693fabe0fde695ee92f4bbdd Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 9 Mar 2022 19:34:23 +0300 Subject: [PATCH 07/10] Move to innerTemplating package --- .../kotlin/renderers/html/HtmlRenderer.kt | 5 +- .../DefaultTemplateModelFactory.kt} | 171 +++++++++--------- .../{ => innerTemplating}/HtmlTemplater.kt | 2 +- 3 files changed, 92 insertions(+), 86 deletions(-) rename plugins/base/src/main/kotlin/renderers/html/{TemplateModelFactory.kt => innerTemplating/DefaultTemplateModelFactory.kt} (58%) rename plugins/base/src/main/kotlin/renderers/html/{ => innerTemplating}/HtmlTemplater.kt (98%) diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 0a1d6bf052..05968576cf 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -6,6 +6,9 @@ import org.jetbrains.dokka.DokkaSourceSetID import org.jetbrains.dokka.base.DokkaBase 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.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.* @@ -33,7 +36,7 @@ open class HtmlRenderer( .filter { it in sourceSet.dependentSourceSets } } - private val templateModelFactory = TemplateModelFactory(context) + private val templateModelFactory = DefaultTemplateModelFactory(context) private val templater = HtmlTemplater(context).apply { setupSharedModel(templateModelFactory.buildSharedModel()) } private var shouldRenderSourceSetBubbles: Boolean = false diff --git a/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt similarity index 58% rename from plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt rename to plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt index 90b736407b..c0590d339b 100644 --- a/plugins/base/src/main/kotlin/renderers/html/TemplateModelFactory.kt +++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt @@ -1,4 +1,4 @@ -package org.jetbrains.dokka.base.renderers.html +package org.jetbrains.dokka.base.renderers.html.innerTemplating import freemarker.core.Environment import freemarker.template.* @@ -8,7 +8,10 @@ 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 @@ -23,7 +26,7 @@ import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.configuration import java.net.URI -class TemplateModelFactory(val context: DokkaContext) { +class DefaultTemplateModelFactory(val context: DokkaContext) { private val configuration = configuration(context) private val isPartial = context.configuration.delayTemplateSubstitution @@ -109,96 +112,96 @@ class TemplateModelFactory(val context: DokkaContext) { } } ?: it) } - - private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { - override fun execute( - env: Environment, - params: MutableMap?, - loopVars: Array?, - 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 PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { + override fun execute( + env: Environment, + params: MutableMap?, + loopVars: Array?, + 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?, - loopVars: Array?, - 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 class TemplateDirective(val configuration: DokkaConfiguration, val pathToRoot: String) : TemplateDirectiveModel { + override fun execute( + env: Environment, + params: MutableMap?, + loopVars: Array?, + 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) ) } - } - - 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) + "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 fun renderWithLocalVar(name: String, value: String, ctx: Context) = - with(ctx) { - env.setVariable(name, SimpleScalar(value)) - body.render(env.out) - env.setVariable(name, null) + 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) + } + } - companion object { - const val PARAM_NAME = "name" - const val PARAM_REPLACEMENT = "replacement" + 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/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt similarity index 98% rename from plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt rename to plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt index 1f6de97d53..b107abc174 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlTemplater.kt +++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt @@ -1,4 +1,4 @@ -package org.jetbrains.dokka.base.renderers.html +package org.jetbrains.dokka.base.renderers.html.innerTemplating import freemarker.cache.ClassTemplateLoader import freemarker.cache.FileTemplateLoader From a9cfd186f92e19d045ad6e4e258027d6c6852eb0 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 9 Mar 2022 19:38:53 +0300 Subject: [PATCH 08/10] Fix doc (articles) --- docs/src/doc/docs/user_guide/base-specific/frontend.md | 6 +++--- 1 file changed, 3 insertions(+), 3 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 5a5caa74af..ecbd696460 100644 --- a/docs/src/doc/docs/user_guide/base-specific/frontend.md +++ b/docs/src/doc/docs/user_guide/base-specific/frontend.md @@ -86,14 +86,14 @@ 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}` - a text that is set by the `footerMessage` property + - `${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/>` - a main content + - `<@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="...""> ...` - 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 a partial outputs from modules. + - `<@template_cmd name="...""> ...` - 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"> From e5b66ba451f8dbfc8401ef11202736a030677970 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 9 Mar 2022 20:06:04 +0300 Subject: [PATCH 09/10] Remove asynchronous generating model --- .../html/innerTemplating/HtmlTemplater.kt | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt index b107abc174..e3d16d98f4 100644 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt +++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt @@ -6,14 +6,11 @@ import freemarker.cache.MultiTemplateLoader import freemarker.log.Logger import freemarker.template.Configuration import freemarker.template.TemplateExceptionHandler -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking 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 -import java.util.concurrent.ConcurrentHashMap enum class DokkaTemplateTypes(val path: String) { @@ -38,9 +35,6 @@ class HtmlTemplater( private val configuration = configuration(context) private val templaterConfiguration = Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() } - private val cachedTemplates: MutableSet = - ConcurrentHashMap().keySet(true) - private fun Configuration.configureTemplateEngine() { val loaderFromResources = ClassTemplateLoader(javaClass, "/dokka/templates") @@ -71,20 +65,11 @@ class HtmlTemplater( generateModel: () -> TemplateMap ): String { val out = StringWriter() - // Freemarker has own cache to keep templates - if (cachedTemplates.contains(templateType)) { // it's a heuristic, freemarker can remove a template from cache - runBlocking { - val templateDeferred = async { templaterConfiguration.getTemplate(templateType.path) } - val model = generateModel() - val template = templateDeferred.await() - cachedTemplates.add(templateType) - template.process(model, out) - } - } else { - val template = templaterConfiguration.getTemplate(templateType.path) - val model = generateModel() - template.process(model, out) - } + // 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() } } From 11494aacfaed0c15b885c6673ca28db221da82d8 Mon Sep 17 00:00:00 2001 From: vmishenev Date: Wed, 9 Mar 2022 20:40:08 +0300 Subject: [PATCH 10/10] Add model merger --- plugins/base/api/base.api | 84 +++++++++++-------- .../kotlin/renderers/html/HtmlRenderer.kt | 24 ++++-- .../DefaultTemplateModelFactory.kt | 6 +- .../DefaultTemplateModelMerger.kt | 16 ++++ .../innerTemplating/TemplateModelFactory.kt | 16 ++++ .../innerTemplating/TemplateModelMerger.kt | 5 ++ 6 files changed, 104 insertions(+), 47 deletions(-) create mode 100644 plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt create mode 100644 plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt create mode 100644 plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 6c9f153952..101a00e9ce 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -335,13 +335,6 @@ public final class org/jetbrains/dokka/base/renderers/html/CustomResourceInstall public fun invoke (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/pages/RootPageNode; } -public final class org/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes : java/lang/Enum { - public static final field BASE Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes; - public final fun getPath ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes; - public static fun values ()[Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes; -} - public final class org/jetbrains/dokka/base/renderers/html/HtmlFormatingUtilsKt { public static final fun buildBreakableDotSeparatedHtml (Lkotlinx/html/FlowContent;Ljava/lang/String;)V public static final fun buildBreakableText (Lkotlinx/html/FlowContent;Ljava/lang/String;)V @@ -400,12 +393,6 @@ public final class org/jetbrains/dokka/base/renderers/html/HtmlRendererKt { public static final fun joinAttr (Ljava/util/List;)Ljava/lang/String; } -public final class org/jetbrains/dokka/base/renderers/html/HtmlTemplater { - public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V - public final fun renderFromTemplate (Lorg/jetbrains/dokka/base/renderers/html/DokkaTemplateTypes;Lkotlin/jvm/functions/Function0;)Ljava/lang/String; - public final fun setupSharedModel (Ljava/util/Map;)V -} - public abstract class org/jetbrains/dokka/base/renderers/html/NavigationDataProvider { public fun ()V public fun navigableChildren (Lorg/jetbrains/dokka/pages/RootPageNode;)Lorg/jetbrains/dokka/base/renderers/html/NavigationNode; @@ -556,28 +543,6 @@ public final class org/jetbrains/dokka/base/renderers/html/TemplateCommand : kot public fun (Ljava/util/Map;Lkotlinx/html/TagConsumer;)V } -public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory { - public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V - public final fun buildModel (Lorg/jetbrains/dokka/pages/PageNode;Ljava/util/List;Lorg/jetbrains/dokka/base/resolvers/local/LocationProvider;ZLjava/lang/String;)Ljava/util/Map; - public final fun buildSharedModel ()Ljava/util/Map; - public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; -} - -public final class org/jetbrains/dokka/base/renderers/html/TemplateModelFactory$SourceSetModel { - public fun (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/TemplateModelFactory$SourceSetModel; - public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$SourceSetModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/renderers/html/TemplateModelFactory$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 class org/jetbrains/dokka/base/renderers/html/WBR : kotlinx/html/HTMLTag, kotlinx/html/HtmlBlockInlineTag { public fun (Ljava/util/Map;Lkotlinx/html/TagConsumer;)V } @@ -619,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 (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 (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 ()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 (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 (Ljava/lang/String;Lorg/jetbrains/dokka/pages/Kind;)V diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 05968576cf..055594697d 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -7,6 +7,7 @@ import org.jetbrains.dokka.base.DokkaBase 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 @@ -36,8 +37,11 @@ open class HtmlRenderer( .filter { it in sourceSet.dependentSourceSets } } - private val templateModelFactory = DefaultTemplateModelFactory(context) - private val templater = HtmlTemplater(context).apply { setupSharedModel(templateModelFactory.buildSharedModel()) } + 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 @@ -784,13 +788,15 @@ open class HtmlRenderer( content() } - templateModelFactory.buildModel( - page, - resources, - locationProvider, - shouldRenderSourceSetBubbles, - generatedContent - ) + templateModelMerger.invoke(templateModelFactories) { + buildModel( + page, + resources, + locationProvider, + shouldRenderSourceSetBubbles, + generatedContent + ) + } } /** diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt index c0590d339b..9f1ca57e70 100644 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt +++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt @@ -26,7 +26,7 @@ import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.configuration import java.net.URI -class DefaultTemplateModelFactory(val context: DokkaContext) { +class DefaultTemplateModelFactory(val context: DokkaContext) : TemplateModelFactory { private val configuration = configuration(context) private val isPartial = context.configuration.delayTemplateSubstitution @@ -36,7 +36,7 @@ class DefaultTemplateModelFactory(val context: DokkaContext) { data class SourceSetModel(val name: String, val platform: String, val filter: String) - fun buildModel( + override fun buildModel( page: PageNode, resources: List, locationProvider: LocationProvider, @@ -78,7 +78,7 @@ class DefaultTemplateModelFactory(val context: DokkaContext) { return mapper } - fun buildSharedModel(): TemplateMap = mapOf( + override fun buildSharedModel(): TemplateMap = mapOf( "footerMessage" to (configuration?.footerMessage?.takeIf { it.isNotEmpty() } ?: DokkaBaseConfiguration.defaultFooterMessage) ) 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 0000000000..7d5487217f --- /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, + buildModel: TemplateModelFactory.() -> TemplateMap + ): TemplateMap { + val mapper = mutableMapOf() + 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/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt new file mode 100644 index 0000000000..ceecf201f5 --- /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, + 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 0000000000..7ad96d8f6e --- /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, buildModel: TemplateModelFactory.() -> TemplateMap): TemplateMap +} \ No newline at end of file