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