diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt index dc877605e9d..fe915f1fcc6 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/github-link.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 3883bc4aed3..72bf7a96631 100644 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt +++ b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt @@ -71,6 +71,8 @@ public class DefaultTemplateModelFactory( } mapper["template_cmd"] = TemplateDirective(context.configuration, pathToRoot) + calculateSourceUrlFromSourceLinks()?.let { mapper["sourceUrl"] = it } + if (page is ContentPage) { val sourceSets = page.content.withDescendants() .flatMap { it.sourceSets } @@ -91,6 +93,30 @@ public class DefaultTemplateModelFactory( ?: DokkaBaseConfiguration.defaultFooterMessage) ) + private fun calculateSourceUrlFromSourceLinks(): String? { + val githubLinkRegex = Regex("http(s)?://github\\.com/([\\w,\\-_]+)/([\\w,\\-_]+)/.*") + + fun parseGithubInfo(link: String): Pair? { + val ( + _, // entire match + _, // optional 's' in http + owner, + repo + ) = githubLinkRegex.find(link)?.groupValues ?: return null + return owner to repo + } + + val (owner, repo) = context.configuration.sourceSets + .asSequence() + .flatMap { it.sourceLinks } + .map { it.remoteUrl.toString() } + .map(::parseGithubInfo) + .distinct() + .singleOrNull() ?: return null + + return "https://github.com/$owner/$repo/" + } + private val DisplaySourceSet.comparableKey get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } private val String.isAbsolute: Boolean @@ -107,6 +133,7 @@ public class DefaultTemplateModelFactory( rel = LinkRel.stylesheet, href = if (resource.isAbsolute) resource else "$pathToRoot$resource" ) + resource.URIExtension == "js" -> script( type = ScriptType.textJavaScript, @@ -117,6 +144,7 @@ public class DefaultTemplateModelFactory( else async = true } + resource.isImage() -> link(href = if (resource.isAbsolute) resource else "$pathToRoot$resource") else -> null } @@ -144,7 +172,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 +201,7 @@ private class TemplateDirective(val configuration: DokkaConfiguration, val pathT Context(env, body) ) } + "projectName" -> { body ?: throw TemplateModelException( "No directive body $commandName command." @@ -183,6 +215,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/github-link.svg b/plugins/base/src/main/resources/dokka/images/github-link.svg new file mode 100644 index 00000000000..ac671baa5f5 --- /dev/null +++ b/plugins/base/src/main/resources/dokka/images/github-link.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/plugins/base/src/main/resources/dokka/styles/style.css b/plugins/base/src/main/resources/dokka/styles/style.css index 67a899a5940..936524cc048 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 SOURCE --- */ +.navigation-controls--source { + height: 40px; + width: 40px; + display: block; + border-radius: 50%; + cursor: pointer; +} + +.navigation-controls--source a::before { + height: 100%; + width: 20px; + margin-left: 10px; + display: block; + content: ""; + background: url("../images/github-link.svg"); + background-size: 100% 100%; +} + +.navigation-controls--source:hover { + background: var(--white-10); +} + +@media (max-width: 759px) { + .navigation-controls--source { + display: none; + } +} +/* /--- Navigation SOURCE --- */ + .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 d5c7a613153..4f0aca8abaa 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 00000000000..449acf5ecc7 --- /dev/null +++ b/plugins/base/src/test/kotlin/renderers/html/HeaderTest.kt @@ -0,0 +1,94 @@ +/* + * 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.SourceLinkDefinitionImpl +import org.jetbrains.dokka.base.renderers.html.HtmlRenderer +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import renderers.testPage +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class HeaderTest : HtmlRenderingOnlyTestBase() { + override val renderedContent: Element + get() = files.contents.getValue("test-page.html").let { Jsoup.parse(it) }.select(".navigation").single() + + @Test + fun `should include link to github if sourceLinks are pointed to github`() { + val sourceLink = SourceLinkDefinitionImpl( + localDirectory = "", + remoteUrl = URL("https://github.com/Kotlin/dokka/tree/main"), + remoteLineSuffix = null + ) + val context = context( + configuration.copy( + sourceSets = configuration.sourceSets.map { + it.copy(sourceLinks = setOf(sourceLink)) + } + ) + ) + + HtmlRenderer(context).render(testPage { }) + + val sourceLinkElement = + assertNotNull(renderedContent.getElementById("source-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 link to github if sourceLinks are different`() { + val sourceLink = SourceLinkDefinitionImpl( + localDirectory = "", + remoteUrl = URL("https://github.com/Kotlin/dokka/tree/main"), + remoteLineSuffix = null + ) + val context = context( + configuration.copy( + sourceSets = listOf( + js.copy(sourceLinks = setOf(sourceLink)), + jvm.copy(sourceLinks = setOf(sourceLink.copy(remoteUrl = URL("https://github.com/Kotlin/dokkatoo/tree/main")))) + ) + ) + ) + + HtmlRenderer(context).render(testPage { }) + + assertNull(renderedContent.getElementById("source-link"), "Source link element found") + } + + @Test + fun `should not include link to github if sourceLinks are pointed to gitlab`() { + val sourceLink = SourceLinkDefinitionImpl( + localDirectory = "", + remoteUrl = URL("https://gitlab.com/Kotlin/dokka/tree/main"), + remoteLineSuffix = null + ) + val context = context( + configuration.copy( + sourceSets = configuration.sourceSets.map { + it.copy(sourceLinks = setOf(sourceLink)) + } + ) + ) + + HtmlRenderer(context).render(testPage { }) + + assertNull(renderedContent.getElementById("source-link"), "Source link element found") + } + + @Test + fun `should not include link to github if there are no sourceLinks`() { + val context = context(configuration) + + HtmlRenderer(context).render(testPage { }) + + assertNull(renderedContent.getElementById("source-link"), "Source link element found") + } +} diff --git a/plugins/base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt b/plugins/base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt index 4e098371e7d..0553c8ab59d 100644 --- a/plugins/base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt +++ b/plugins/base/src/test/kotlin/renderers/html/HtmlRenderingOnlyTestBase.kt @@ -4,6 +4,7 @@ package renderers.html +import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.DokkaConfigurationImpl import org.jetbrains.dokka.Platform import org.jetbrains.dokka.base.DokkaBase @@ -48,13 +49,15 @@ abstract class HtmlRenderingOnlyTestBase : RenderingOnlyTestBase() { finalizeCoroutines = false ) - override val context = MockContext( + override val context by lazy { context(configuration) } + + fun context(testConfiguration: DokkaConfiguration): MockContext = MockContext( DokkaBase().outputWriter to { files }, DokkaBase().locationProviderFactory to ::DokkaLocationProviderFactory, DokkaBase().htmlPreprocessors to { RootCreator }, DokkaBase().externalLocationProviderFactory to ::JavadocExternalLocationProviderFactory, DokkaBase().externalLocationProviderFactory to ::DefaultExternalLocationProviderFactory, - testConfiguration = configuration + testConfiguration = testConfiguration ) override val renderedContent: Element by lazy {