From 095fb92ebb6e284aab53667548648a8fe791f621 Mon Sep 17 00:00:00 2001 From: IgnatBeresnev Date: Fri, 29 Jul 2022 20:46:21 +0200 Subject: [PATCH 1/2] Render nested classlikes in navigation --- .../renderers/html/NavigationDataProvider.kt | 15 +- .../kotlin/renderers/html/NavigationPage.kt | 7 +- .../renderers/html/NavigationIconTest.kt | 12 +- .../kotlin/renderers/html/NavigationTest.kt | 228 ++++++++++++++++++ .../base/src/test/kotlin/utils/HtmlUtils.kt | 11 + 5 files changed, 249 insertions(+), 24 deletions(-) create mode 100644 plugins/base/src/test/kotlin/renderers/html/NavigationTest.kt create mode 100644 plugins/base/src/test/kotlin/utils/HtmlUtils.kt diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt index 647ba6874d..f026518d93 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt @@ -73,18 +73,17 @@ abstract class NavigationDataProvider { } private fun ContentPage.navigableChildren(): List { - return if (this !is ClasslikePageNode) { + return if (this is ClasslikePage) { + // Classlikes should only have other classlikes as navigable children children - .filterIsInstance() + .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) } } else { - emptyList() + children + .filterIsInstance() + .map { visit(it) } + .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() +} From e6ffc1187769efa6e05597c662b79dff06db02e6 Mon Sep 17 00:00:00 2001 From: IgnatBeresnev Date: Wed, 3 Aug 2022 15:06:00 +0200 Subject: [PATCH 2/2] Fix enum entry ordering in navigation tree --- .../renderers/html/NavigationDataProvider.kt | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt index f026518d93..958488efa6 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt @@ -74,11 +74,7 @@ abstract class NavigationDataProvider { private fun ContentPage.navigableChildren(): List { return if (this is ClasslikePage) { - // Classlikes should only have other classlikes as navigable children - children - .filterIsInstance() - .map { visit(it) } - .sortedBy { it.name.toLowerCase() } + return this.navigableChildren() } else { children .filterIsInstance() @@ -86,4 +82,19 @@ abstract class NavigationDataProvider { .sortedBy { it.name.toLowerCase() } } } + + 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 { + navigableChildren.sortedBy { it.name.toLowerCase() } + } + } }