Skip to content

Commit

Permalink
Render nested classlikes in navigation (#2597)
Browse files Browse the repository at this point in the history
  • Loading branch information
IgnatBeresnev committed Aug 4, 2022
1 parent 01cc092 commit 7b020f0
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 23 deletions.
Expand Up @@ -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() }
}
}
}
Expand Up @@ -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
Expand All @@ -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(
Expand Down
12 changes: 2 additions & 10 deletions 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() {

Expand Down Expand Up @@ -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()
}
}
228 changes: 228 additions & 0 deletions 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]}")
}
}
}
11 changes: 11 additions & 0 deletions 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()
}

0 comments on commit 7b020f0

Please sign in to comment.