dokka icon indicating copy to clipboard operation
dokka copied to clipboard

Support hiding extension functions from the TOC (side menu)

Open OliverO2 opened this issue 3 weeks ago • 3 comments

Is your feature request related to a problem? Please describe

Use case: TestBalloon's API consists of a large number of extension functions, mostly on TestConfig, which is a central configuration builder. These extension functions are not meaningful outside this builder, so seeing them in the de.infix.testBalloon.framework.core package's top-level navigation is just noise cluttering the signal (relevant content):

Image

Describe the solution you'd like

Hiding such extension functions from the TOC shows more relevant content (declarations which are actually accessible at the top level):

Image

Describe alternatives you've considered

This Dokka plugin hides the affected navigation nodes (and also fixes an empty package remaining after applying another plugin to hide internal-API-annotated symbols):

/**
 * A Dokka plugin which hides extension functions and empty packages in the table of contents (side menu).
 */
class NavigationNodeHidingDokkaPlugin : DokkaPlugin() {
    @Suppress("unused")
    val myFilterExtension: Extension<PageTransformer, *, *> by extending {
        val dokkaBase = plugin<DokkaBase>()
        dokkaBase.htmlPreprocessors providing ::NavigationNodeHidingTransformer override
            dokkaBase.navigationPageInstaller
    }

    @DokkaPluginApiPreview
    override fun pluginApiPreviewAcknowledgement() = PluginApiPreviewAcknowledgement
}

private class NavigationNodeHidingTransformer(context: DokkaContext) : NavigationPageInstaller(context) {
    override fun navigableChildren(input: RootPageNode): NavigationNode = super.navigableChildren(input).transform {
        NavigationNode(
            it.name,
            it.dri,
            it.sourceSets,
            it.icon,
            it.styles,
            it.children.filterNot { child ->
                // Drop extension functions and empty packages.
                child.isExtensionFunction() || child.isEmptyPackage()
            }
        )
    }

    private fun NavigationNode.isExtensionFunction(): Boolean = dri.callable?.receiver != null

    private fun NavigationNode.isEmptyPackage(): Boolean {
        val isPackage = dri.classNames == null && dri.callable == null && dri.packageName != null
        return isPackage && children.isEmpty()
    }
}

Additional context

Slack discussion: https://kotlinlang.slack.com/archives/C0F4UNJET/p1761560392906549?thread_ts=1761330952.244459&cid=C0F4UNJET

OliverO2 avatar Dec 05 '25 15:12 OliverO2

Thanks! That's really a nice idea to explore!

Though, regarding your current implementation, you do drop all extensions even if they have different receivers. For example, if you had a function like fun String.xxx() at top-level, the declaration will be hidden and not shown in documentation at all. To be correct, you might want to limit filtering to types coming from the current module only.

whyoleg avatar Dec 10 '25 09:12 whyoleg

Thanks for the feedback! Yes you are right that the above plugin would potentially hide too much. It's not an issue now, as there are no public extensions on outside types, but this could change, of course. I'll take care of this.

Ideally, even with such outside types, I'd prefer to treat extensions as if they were members. Given your String.xxx() example, is there a way to make String appear as a top-level type in the navigation? I know it's not "mine" and it would be incomplete, showing only "my" extensions, but I'd say even then it provides better explorability than having the extension-less function xxx() on the top level.

OliverO2 avatar Dec 10 '25 12:12 OliverO2

Ideally, even with such outside types, I'd prefer to treat extensions as if they were members. Given your String.xxx() example, is there a way to make String appear as a top-level type in the navigation?

Theoretically, it's possible (as Dokka is very flexible :)). You will need to collect extensions (as in ExtensionExtractorTransformer) for external types and create additional ContentPages for them.

Additionally, Swift Docc utilizes this approach with Extended Modules / Extended Protocols.

whyoleg avatar Dec 10 '25 14:12 whyoleg