diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2022-08-04 12:43:54 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-04 12:43:54 +0200 |
commit | 7b020f000aa7ea868d5d3037e68eaec621ef9972 (patch) | |
tree | cc041fa115272920472204492277b306bc5bcf6e /plugins | |
parent | 01cc092fed1b4de81b6b39c147e162575b86dfd0 (diff) | |
download | dokka-7b020f000aa7ea868d5d3037e68eaec621ef9972.tar.gz dokka-7b020f000aa7ea868d5d3037e68eaec621ef9972.tar.bz2 dokka-7b020f000aa7ea868d5d3037e68eaec621ef9972.zip |
Render nested classlikes in navigation (#2597)
Diffstat (limited to 'plugins')
5 files changed, 259 insertions, 23 deletions
diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt index 647ba687..958488ef 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<NavigationNode> { - return if (this !is ClasslikePageNode) { + return if (this is ClasslikePage) { + return this.navigableChildren() + } else { children .filterIsInstance<ContentPage>() .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<NavigationNode> { + // Classlikes should only have other classlikes as navigable children + val navigableChildren = children + .filterIsInstance<ClasslikePage>() + .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 e5183699..87808add 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 f2c1fca8..a7a7bacf 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 00000000..104246cb --- /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 00000000..bfba882a --- /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() +} |