Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render nested classlikes in navigation #2597

Merged
merged 2 commits into from Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()
}