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..ecbd696460 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="...""> ...` - 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"> + ${projectName} + + ``` \ 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..101a00e9ce 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 } @@ -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 (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/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..055594697d 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(context) { - private val configuration = configuration(context) - private val sourceSetDependencyMap: Map> = 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().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, 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, 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 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/innerTemplating/DefaultTemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt new file mode 100644 index 0000000000..9f1ca57e70 --- /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(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) + + override 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["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( + "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) + } +} + +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 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 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/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt new file mode 100644 index 0000000000..e3d16d98f4 --- /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 + +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() } + + 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 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 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