From 64cce58f73f908909ba60da6005d7abb2572b2f0 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Tue, 7 Nov 2023 15:27:34 +0200 Subject: Add the link to GitHub repo to the header if there are source links defined (#3235) * Use URL from base plugin configuration * Add integration test for the multi-module project that the homepage link exists everywhere --- .../it-multimodule-0/moduleA/build.gradle.kts | 10 ++ .../dokka/it/gradle/MultiModule0IntegrationTest.kt | 15 +++ plugins/base/api/base.api | 11 ++- .../base/src/main/kotlin/DokkaBaseConfiguration.kt | 3 +- .../kotlin/renderers/html/htmlPreprocessors.kt | 1 + .../innerTemplating/DefaultTemplateModelFactory.kt | 24 ++++- .../src/main/resources/dokka/images/homepage.svg | 5 + .../base/src/main/resources/dokka/styles/style.css | 32 ++++++- .../resources/dokka/templates/includes/header.ftl | 3 + .../src/test/kotlin/renderers/html/HeaderTest.kt | 102 +++++++++++++++++++++ 10 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 plugins/base/src/main/resources/dokka/images/homepage.svg create mode 100644 plugins/base/src/test/kotlin/renderers/html/HeaderTest.kt diff --git a/integration-tests/gradle/projects/it-multimodule-0/moduleA/build.gradle.kts b/integration-tests/gradle/projects/it-multimodule-0/moduleA/build.gradle.kts index 1e61f8b2..d24b90c5 100644 --- a/integration-tests/gradle/projects/it-multimodule-0/moduleA/build.gradle.kts +++ b/integration-tests/gradle/projects/it-multimodule-0/moduleA/build.gradle.kts @@ -8,3 +8,13 @@ plugins { kotlin("jvm") id("org.jetbrains.dokka") } + +allprojects { + tasks.withType { + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to """{ "homepageLink" : "https://github.com/Kotlin/dokka/tree/master/integration-tests/gradle/projects/it-multimodule-0/" }""" + ) + ) + } +} diff --git a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/MultiModule0IntegrationTest.kt b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/MultiModule0IntegrationTest.kt index 54ac3ff8..f4061345 100644 --- a/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/MultiModule0IntegrationTest.kt +++ b/integration-tests/gradle/src/integrationTest/kotlin/org/jetbrains/dokka/it/gradle/MultiModule0IntegrationTest.kt @@ -73,6 +73,21 @@ class MultiModule0IntegrationTest : AbstractGradleIntegrationTest() { "Expected moduleC being mentioned in -modules.html" ) + val htmlsWithHomepageLink = outputDir.walkTopDown().filter { + it.isFile && it.extension == "html" && it.name != "navigation.html" + }.toList() + + assertEquals(16, htmlsWithHomepageLink.size) + + htmlsWithHomepageLink.forEach { + assertTrue( + it.readText().contains( + """https://github.com/Kotlin/dokka/tree/master/integration-tests/gradle/projects/it-multimodule-0/""" + ), + "File ${it.absolutePath} doesn't contain link to homepage" + ) + } + val gfmOutputDir = File(projectDir, "moduleA/build/dokka/gfmMultiModule") assertTrue(gfmOutputDir.isDirectory, "Missing dokka GFM output directory") diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index ae872558..13f877e3 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -105,20 +105,22 @@ 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;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 fun (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;Ljava/lang/String;)V + public synthetic fun (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;Ljava/lang/String;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 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 final fun component7 ()Ljava/lang/String; + public final fun copy (Ljava/util/List;Ljava/util/List;ZLjava/lang/String;ZLjava/io/File;Ljava/lang/String;)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;Ljava/lang/String;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 getHomepageLink ()Ljava/lang/String; public final fun getMergeImplicitExpectActualDeclarations ()Z public final fun getSeparateInheritedMembers ()Z public final fun getTemplatesDir ()Ljava/io/File; @@ -126,6 +128,7 @@ public final class org/jetbrains/dokka/base/DokkaBaseConfiguration : org/jetbrai 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 setHomepageLink (Ljava/lang/String;)V public final fun setMergeImplicitExpectActualDeclarations (Z)V public final fun setSeparateInheritedMembers (Z)V public final fun setTemplatesDir (Ljava/io/File;)V diff --git a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt index 11184126..34195f65 100644 --- a/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt +++ b/plugins/base/src/main/kotlin/DokkaBaseConfiguration.kt @@ -14,7 +14,8 @@ public data class DokkaBaseConfiguration( var separateInheritedMembers: Boolean = separateInheritedMembersDefault, var footerMessage: String = defaultFooterMessage, var mergeImplicitExpectActualDeclarations: Boolean = mergeImplicitExpectActualDeclarationsDefault, - var templatesDir: File? = defaultTemplatesDir + var templatesDir: File? = defaultTemplatesDir, + var homepageLink: String? = null, ) : ConfigurableBlock { public companion object { public val defaultFooterMessage: String = "© ${Year.now().value} Copyright" diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt index dc877605..dad013e2 100644 --- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -111,6 +111,7 @@ public object AssetsInstaller : PageTransformer { "images/copy-successful-icon.svg", "images/theme-toggle.svg", "images/burger.svg", + "images/homepage.svg", // navigation icons "images/nav-icons/abstract-class.svg", 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 3883bc4a..fe6f0089 100644 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt +++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt @@ -86,10 +86,16 @@ public class DefaultTemplateModelFactory( return mapper } - override fun buildSharedModel(): TemplateMap = mapOf( - "footerMessage" to (configuration?.footerMessage?.takeIf { it.isNotEmpty() } - ?: DokkaBaseConfiguration.defaultFooterMessage) - ) + override fun buildSharedModel(): TemplateMap { + val mapper = mutableMapOf() + + mapper["footerMessage"] = + (configuration?.footerMessage?.takeIf(String::isNotBlank) ?: DokkaBaseConfiguration.defaultFooterMessage) + + configuration?.homepageLink?.takeIf(String::isNotBlank)?.let { mapper["homepageLink"] = it } + + return mapper + } private val DisplaySourceSet.comparableKey get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } @@ -107,6 +113,7 @@ public class DefaultTemplateModelFactory( rel = LinkRel.stylesheet, href = if (resource.isAbsolute) resource else "$pathToRoot$resource" ) + resource.URIExtension == "js" -> script( type = ScriptType.textJavaScript, @@ -117,6 +124,7 @@ public class DefaultTemplateModelFactory( else async = true } + resource.isImage() -> link(href = if (resource.isAbsolute) resource else "$pathToRoot$resource") else -> null } @@ -125,6 +133,7 @@ public class DefaultTemplateModelFactory( append(resourceHtml) } } + } private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { @@ -144,7 +153,10 @@ private class PrintDirective(val generateData: () -> String) : TemplateDirective } } -private 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?, @@ -170,6 +182,7 @@ private class TemplateDirective(val configuration: DokkaConfiguration, val pathT Context(env, body) ) } + "projectName" -> { body ?: throw TemplateModelException( "No directive body $commandName command." @@ -183,6 +196,7 @@ private class TemplateDirective(val configuration: DokkaConfiguration, val pathT Context(env, body) ) } + else -> throw TemplateModelException( "The parameter $PARAM_NAME $commandName is unknown" ) diff --git a/plugins/base/src/main/resources/dokka/images/homepage.svg b/plugins/base/src/main/resources/dokka/images/homepage.svg new file mode 100644 index 00000000..a3d7602b --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/homepage.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/base/src/main/resources/dokka/styles/style.css b/plugins/base/src/main/resources/dokka/styles/style.css index 67a899a5..62b0ddbd 100644 --- a/plugins/base/src/main/resources/dokka/styles/style.css +++ b/plugins/base/src/main/resources/dokka/styles/style.css @@ -342,6 +342,7 @@ td:first-child { /* --- Navigation controls --- */ .navigation-controls { display: flex; + margin-left: 4px; } @media (min-width: 760px) { @@ -365,7 +366,6 @@ td:first-child { display: block; border-radius: 50%; background-color: inherit; - margin-left: 4px; padding: 0; border: none; cursor: pointer; @@ -394,6 +394,36 @@ td:first-child { } /* /--- Navigation THEME --- */ +/* --- Navigation HOMEPAGE --- */ +.navigation-controls--homepage { + height: 40px; + width: 40px; + display: block; + border-radius: 50%; + cursor: pointer; +} + +.navigation-controls--homepage a::before { + height: 100%; + width: 20px; + margin-left: 10px; + display: block; + content: ""; + background: url("../images/homepage.svg"); + background-size: 100% 100%; +} + +.navigation-controls--homepage:hover { + background: var(--white-10); +} + +@media (max-width: 759px) { + .navigation-controls--homepage { + display: none; + } +} +/* /--- Navigation HOMEPAGE --- */ + .navigation .platform-selector:not([data-active]) { color: #fff; } diff --git a/plugins/base/src/main/resources/dokka/templates/includes/header.ftl b/plugins/base/src/main/resources/dokka/templates/includes/header.ftl index d5c7a613..d399e633 100644 --- a/plugins/base/src/main/resources/dokka/templates/includes/header.ftl +++ b/plugins/base/src/main/resources/dokka/templates/includes/header.ftl @@ -21,6 +21,9 @@ <@source_set_selector.display/> diff --git a/plugins/base/src/test/kotlin/renderers/html/HeaderTest.kt b/plugins/base/src/test/kotlin/renderers/html/HeaderTest.kt new file mode 100644 index 00000000..c19f965f --- /dev/null +++ b/plugins/base/src/test/kotlin/renderers/html/HeaderTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package renderers.html + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.DokkaBaseConfiguration +import org.jetbrains.dokka.base.templating.toJsonString +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.plugability.DokkaContext +import org.jsoup.Jsoup +import utils.TestOutputWriter +import utils.TestOutputWriterPlugin +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class HeaderTest : BaseAbstractTest() { + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + name = "jvm" + sourceRoots = listOf("src/jvm") + } + sourceSet { + name = "js" + sourceRoots = listOf("src/js") + } + } + } + + @Test + fun `should include homepage link if homepageLink is provided`() { + testRendering( + DokkaBaseConfiguration(homepageLink = "https://github.com/Kotlin/dokka/") + ) { _, _, writer -> + val renderedContent = navigationElement(writer) + + val sourceLinkElement = + assertNotNull(renderedContent.getElementById("homepage-link"), "Source link element not found") + val aElement = assertNotNull(sourceLinkElement.selectFirst("a")) + assertEquals("https://github.com/Kotlin/dokka/", aElement.attr("href")) + } + } + + @Test + fun `should not include homepage link by default`() { + testRendering(null) { _, _, writer -> + val renderedContent = navigationElement(writer) + assertNull(renderedContent.getElementById("homepage-link"), "Source link element found") + } + } + + private fun testRendering( + baseConfiguration: DokkaBaseConfiguration?, + block: (RootPageNode, DokkaContext, writer: TestOutputWriter) -> Unit + ) { + fun configuration(): DokkaConfigurationImpl { + baseConfiguration ?: return configuration + return configuration.copy( + pluginsConfiguration = listOf( + PluginConfigurationImpl( + DokkaBase::class.java.canonicalName, + DokkaConfiguration.SerializationFormat.JSON, + toJsonString(baseConfiguration) + ) + ) + ) + } + + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/jvm/Test.kt + |fun test() {} + |/src/js/Test.kt + |fun test() {} + """, + configuration(), + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { node, context -> + block(node, context, writerPlugin.writer) + } + } + } + + private fun navigationElement(writer: TestOutputWriter) = + writer + .contents + .getValue("index.html") + .let(Jsoup::parse) + .select(".navigation") + .single() + +} -- cgit