diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt index 647ba6874d..958488efa6 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt @@ -73,18 +73,28 @@ abstract class NavigationDataProvider { } private fun ContentPage.navigableChildren(): List { - return if (this !is ClasslikePageNode) { + return if (this is ClasslikePage) { + return this.navigableChildren() + } else { children .filterIsInstance() .map { visit(it) } .sortedBy { it.name.toLowerCase() } - } else if (documentables.any { it is DEnum }) { - // no sorting for enum entries, should be the same as in source code - children - .filter { child -> child is WithDocumentables && child.documentables.any { it is DEnumEntry } } - .map { visit(it as ContentPage) } + } + } + + private fun ClasslikePage.navigableChildren(): List { + // Classlikes should only have other classlikes as navigable children + val navigableChildren = children + .filterIsInstance() + .map { visit(it) } + + val isEnumPage = documentables.any { it is DEnum } + return if (isEnumPage) { + // no sorting for enum entries, should be the same order as in source code + navigableChildren } else { - emptyList() + navigableChildren.sortedBy { it.name.toLowerCase() } } } } diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt index e51836997a..87808adde4 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -49,8 +49,7 @@ class NavigationPage( } } buildLink(node.dri, node.sourceSets.toList()) { - // special condition for Enums as it has children enum entries in navigation - val withIcon = node.icon != null && (node.children.isEmpty() || node.isEnum()) + val withIcon = node.icon != null if (withIcon) { // in case link text is so long that it needs to have word breaks, // and it stretches to two or more lines, make sure the icon @@ -69,10 +68,6 @@ class NavigationPage( node.children.withIndex().forEach { (n, p) -> visit(p, "$navId-$n", renderer) } } } - - private fun NavigationNode.isEnum(): Boolean { - return icon == NavigationNodeIcon.ENUM_CLASS || icon == NavigationNodeIcon.ENUM_CLASS_KT - } } data class NavigationNode( diff --git a/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt b/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt index f2c1fca84c..a7a7bacf62 100644 --- a/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt +++ b/plugins/base/src/test/kotlin/renderers/html/NavigationIconTest.kt @@ -1,13 +1,11 @@ package renderers.html import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest -import org.jsoup.Jsoup -import org.jsoup.nodes.Element -import org.jsoup.select.Elements import org.junit.jupiter.api.Test -import utils.TestOutputWriter import utils.TestOutputWriterPlugin import kotlin.test.assertEquals +import utils.navigationHtml +import utils.selectNavigationGrid class NavigationIconTest : BaseAbstractTest() { @@ -277,10 +275,4 @@ class NavigationIconTest : BaseAbstractTest() { } } } - - private fun TestOutputWriter.navigationHtml(): Element = contents.getValue("navigation.html").let { Jsoup.parse(it) } - - private fun Elements.selectNavigationGrid(): Element { - return this.select("div.overview").select("span.nav-link-grid").single() - } } diff --git a/plugins/base/src/test/kotlin/renderers/html/NavigationTest.kt b/plugins/base/src/test/kotlin/renderers/html/NavigationTest.kt new file mode 100644 index 0000000000..104246cb45 --- /dev/null +++ b/plugins/base/src/test/kotlin/renderers/html/NavigationTest.kt @@ -0,0 +1,228 @@ +package renderers.html + +import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon +import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jsoup.nodes.Element +import org.junit.jupiter.api.Test +import utils.TestOutputWriterPlugin +import kotlin.test.assertEquals +import utils.navigationHtml + +class NavigationTest : BaseAbstractTest() { + + private val configuration = dokkaConfiguration { + sourceSets { + sourceSet { + sourceRoots = listOf("src/") + } + } + } + + @Test + fun `should have expandable classlikes`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/WithInner.kt + |package com.example + | + |class WithInner { + | // in-class functions should not be in navigation + | fun a() {} + | fun b() {} + | fun c() {} + | + | class InnerClass {} + | interface InnerInterface {} + | enum class InnerEnum {} + | object InnerObject {} + | annotation class InnerAnnotation {} + | companion object CompanionObject {} + |} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(9, content.size) + + // Navigation menu should be the following, sorted by name: + // - root + // - com.example + // - WithInner + // - CompanionObject + // - InnerAnnotation + // - InnerClass + // - InnerEnum + // - InnerInterface + // - InnerObject + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "WithInner", + address = "root/com.example/-with-inner/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[3].assertNavigationLink( + id = "root-nav-submenu-0-0-0", + text = "CompanionObject", + address = "root/com.example/-with-inner/-companion-object/index.html", + icon = NavigationNodeIcon.OBJECT + ) + + content[4].assertNavigationLink( + id = "root-nav-submenu-0-0-1", + text = "InnerAnnotation", + address = "root/com.example/-with-inner/-inner-annotation/index.html", + icon = NavigationNodeIcon.ANNOTATION_CLASS_KT + ) + + content[5].assertNavigationLink( + id = "root-nav-submenu-0-0-2", + text = "InnerClass", + address = "root/com.example/-with-inner/-inner-class/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[6].assertNavigationLink( + id = "root-nav-submenu-0-0-3", + text = "InnerEnum", + address = "root/com.example/-with-inner/-inner-enum/index.html", + icon = NavigationNodeIcon.ENUM_CLASS_KT + ) + + content[7].assertNavigationLink( + id = "root-nav-submenu-0-0-4", + text = "InnerInterface", + address = "root/com.example/-with-inner/-inner-interface/index.html", + icon = NavigationNodeIcon.INTERFACE_KT + ) + + content[8].assertNavigationLink( + id = "root-nav-submenu-0-0-5", + text = "InnerObject", + address = "root/com.example/-with-inner/-inner-object/index.html", + icon = NavigationNodeIcon.OBJECT + ) + } + } + } + + @Test + fun `should be able to have deeply nested classlikes`() { + val writerPlugin = TestOutputWriterPlugin() + testInline( + """ + |/src/main/kotlin/com/example/DeeplyNested.kt + |package com.example + | + |class DeeplyNested { + | class FirstLevelClass { + | interface SecondLevelInterface { + | object ThirdLevelObject { + | annotation class FourthLevelAnnotation {} + | } + | } + | } + |} + """.trimIndent(), + configuration, + pluginOverrides = listOf(writerPlugin) + ) { + renderingStage = { _, _ -> + val content = writerPlugin.writer.navigationHtml().select("div.sideMenuPart") + assertEquals(7, content.size) + + // Navigation menu should be the following + // - root + // - com.example + // - DeeplyNested + // - FirstLevelClass + // - SecondLevelInterface + // - ThirdLevelObject + // - FourthLevelAnnotation + + content[0].assertNavigationLink( + id = "root-nav-submenu", + text = "root", + address = "index.html", + ) + + content[1].assertNavigationLink( + id = "root-nav-submenu-0", + text = "com.example", + address = "root/com.example/index.html", + ) + + content[2].assertNavigationLink( + id = "root-nav-submenu-0-0", + text = "DeeplyNested", + address = "root/com.example/-deeply-nested/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[3].assertNavigationLink( + id = "root-nav-submenu-0-0-0", + text = "FirstLevelClass", + address = "root/com.example/-deeply-nested/-first-level-class/index.html", + icon = NavigationNodeIcon.CLASS_KT + ) + + content[4].assertNavigationLink( + id = "root-nav-submenu-0-0-0-0", + text = "SecondLevelInterface", + address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/index.html", + icon = NavigationNodeIcon.INTERFACE_KT + ) + + content[5].assertNavigationLink( + id = "root-nav-submenu-0-0-0-0-0", + text = "ThirdLevelObject", + address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/" + + "-third-level-object/index.html", + icon = NavigationNodeIcon.OBJECT + ) + + content[6].assertNavigationLink( + id = "root-nav-submenu-0-0-0-0-0-0", + text = "FourthLevelAnnotation", + address = "root/com.example/-deeply-nested/-first-level-class/-second-level-interface/" + + "-third-level-object/-fourth-level-annotation/index.html", + icon = NavigationNodeIcon.ANNOTATION_CLASS_KT + ) + } + } + } + + private fun Element.assertNavigationLink( + id: String, text: String, address: String, icon: NavigationNodeIcon? = null + ) { + assertEquals(id, this.id()) + + val link = this.selectFirst("a") + checkNotNull(link) + assertEquals(text, link.text()) + assertEquals(address, link.attr("href")) + if (icon != null) { + val iconStyles = + this.selectFirst("div.overview span.nav-link-grid")?.child(0)?.classNames()?.toList() ?: emptyList() + assertEquals(3, iconStyles.size) + assertEquals("nav-link-child", iconStyles[0]) + assertEquals(icon.style(), "${iconStyles[1]} ${iconStyles[2]}") + } + } +} diff --git a/plugins/base/src/test/kotlin/utils/HtmlUtils.kt b/plugins/base/src/test/kotlin/utils/HtmlUtils.kt new file mode 100644 index 0000000000..bfba882a11 --- /dev/null +++ b/plugins/base/src/test/kotlin/utils/HtmlUtils.kt @@ -0,0 +1,11 @@ +package utils + +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.select.Elements + +internal fun TestOutputWriter.navigationHtml(): Element = contents.getValue("navigation.html").let { Jsoup.parse(it) } + +internal fun Elements.selectNavigationGrid(): Element { + return this.select("div.overview").select("span.nav-link-grid").single() +}