All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.dokka.base.renderers.html.HtmlRenderer.kt Maven / Gradle / Ivy

There is a newer version: 2.0.0
Show newest version
/*
 * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package org.jetbrains.dokka.base.renderers.html

import kotlinx.html.*
import kotlinx.html.consumers.delayed
import kotlinx.html.consumers.onFinalizeMap
import kotlinx.html.stream.HTMLStreamBuilder
import kotlinx.html.stream.createHTML
import org.jetbrains.dokka.DokkaSourceSetID
import org.jetbrains.dokka.Platform
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.renderers.*
import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer
import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelFactory
import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelMerger
import org.jetbrains.dokka.base.renderers.html.innerTemplating.DokkaTemplateTypes
import org.jetbrains.dokka.base.renderers.html.innerTemplating.HtmlTemplater
import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint
import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider
import org.jetbrains.dokka.base.templating.*
import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions
import org.jetbrains.dokka.base.pages.AllTypesPageNode
import org.jetbrains.dokka.base.translators.documentables.shouldDocumentConstructors
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.model.properties.WithExtraProperties
import org.jetbrains.dokka.pages.*
import org.jetbrains.dokka.pages.HtmlContent
import org.jetbrains.dokka.plugability.*
import org.jetbrains.dokka.transformers.pages.PageTransformer
import org.jetbrains.dokka.utilities.htmlEscape

internal const val TEMPLATE_REPLACEMENT: String = "###"
internal const val TOGGLEABLE_CONTENT_TYPE_ATTR = "data-togglable"

public open class HtmlRenderer(
    context: DokkaContext
) : DefaultRenderer(context) {
    private val sourceSetDependencyMap: Map> =
        context.configuration.sourceSets.associate { sourceSet ->
            sourceSet.sourceSetID to context.configuration.sourceSets
                .map { it.sourceSetID }
                .filter { it in sourceSet.dependentSourceSets }
        }

    private val templateModelFactories = listOf(DefaultTemplateModelFactory(context)) // TODO: Make extension point
    private val templateModelMerger = DefaultTemplateModelMerger()
    private val templater = HtmlTemplater(context).apply {
        setupSharedModel(templateModelMerger.invoke(templateModelFactories) { buildSharedModel() })
    }

    private var shouldRenderSourceSetTabs: Boolean = false

    override val preprocessors: List = context.plugin().query { htmlPreprocessors }
    private val customCodeBlockRenderers = context.plugin().query { htmlCodeBlockRenderers }

    /**
     * Tabs themselves are created in HTML plugin since, currently, only HTML format supports them.
     * [TabbedContentType] is used to mark content that should be inside tab content.
     * A tab can display multiple [TabbedContentType].
     * The content style [ContentStyle.TabbedContent] is used to determine where tabs will be generated.
     *
     * @see TabbedContentType
     * @see ContentStyle.TabbedContent
     */
    private fun createTabs(pageContext: ContentPage): List {
        return when(pageContext) {
            is ClasslikePage -> createTabsForClasslikes(pageContext)
            is PackagePage -> createTabsForPackage(pageContext)
            else -> throw IllegalArgumentException("Page ${pageContext.name} cannot have tabs")
        }
    }

    private fun createTabsForClasslikes(page: ClasslikePage): List {
        val documentables = page.documentables
        val csEnum = documentables.filterIsInstance()
        val csWithConstructor = documentables.filterIsInstance()
        val scopes = documentables.filterIsInstance()
        val constructorsToDocumented = csWithConstructor.flatMap { it.constructors }

        val containsRenderableConstructors = constructorsToDocumented.isNotEmpty() && documentables.shouldDocumentConstructors()
        val containsRenderableMembers =
            containsRenderableConstructors || scopes.any { it.classlikes.isNotEmpty() || it.functions.isNotEmpty() || it.properties.isNotEmpty() }

        @Suppress("UNCHECKED_CAST")
        val extensions = (documentables as List>).flatMap {
            it.extra[CallableExtensions]?.extensions
                ?.filterIsInstance().orEmpty()
        }
            .distinctBy { it.sourceSets to it.dri } // [Documentable] has expensive equals/hashCode at the moment, see #2620
        return listOfNotNull(
            if(!containsRenderableMembers) null else
                ContentTab(
                    "Members",
                    listOf(
                        BasicTabbedContentType.CONSTRUCTOR,
                        BasicTabbedContentType.TYPE,
                        BasicTabbedContentType.PROPERTY,
                        BasicTabbedContentType.FUNCTION
                    )
                ),
            if (extensions.isEmpty()) null else ContentTab(
                "Members & Extensions",
                listOf(
                    BasicTabbedContentType.CONSTRUCTOR,
                    BasicTabbedContentType.TYPE,
                    BasicTabbedContentType.PROPERTY,
                    BasicTabbedContentType.FUNCTION,
                    BasicTabbedContentType.EXTENSION_PROPERTY,
                    BasicTabbedContentType.EXTENSION_FUNCTION
                )
            ),
            if(csEnum.isEmpty()) null else ContentTab(
                "Entries",
                listOf(
                    BasicTabbedContentType.ENTRY
                )
            )
        )
    }

    private fun createTabsForPackage(page: PackagePage): List {
        val p = page.documentables.single() as DPackage
        return listOfNotNull(
            if (p.typealiases.isEmpty() && p.classlikes.isEmpty()) null else ContentTab(
                "Types",
                listOf(
                    BasicTabbedContentType.TYPE,
                )
            ),
            if (p.functions.isEmpty()) null else ContentTab(
                "Functions",
                listOf(
                    BasicTabbedContentType.FUNCTION,
                    BasicTabbedContentType.EXTENSION_FUNCTION,
                )
            ),
            if (p.properties.isEmpty()) null else ContentTab(
                "Properties",
                listOf(
                    BasicTabbedContentType.PROPERTY,
                    BasicTabbedContentType.EXTENSION_PROPERTY,
                )
            )
        )
    }

    private fun  TagConsumer.prepareForTemplates() =
        if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this
        else ImmediateResolutionTagConsumer(this, context)

    override fun FlowContent.wrapGroup(
        node: ContentGroup,
        pageContext: ContentPage,
        childrenCallback: FlowContent.() -> Unit
    ) {
        val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() }
        return when {
            node.hasStyle(ContentStyle.TabbedContent) -> div(additionalClasses) {
                val contentTabs = createTabs(pageContext)

                div(classes = "tabs-section") {
                    attributes["tabs-section"] = "tabs-section"
                    contentTabs.forEachIndexed { index, contentTab ->
                        button(classes = "section-tab") {
                            if (index == 0) attributes["data-active"] = ""
                            attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] =
                                contentTab.tabbedContentTypes.joinToString(",") { it.toHtmlAttribute() }
                            text(contentTab.text)
                        }
                    }
                }
                div(classes = "tabs-section-body") {
                    childrenCallback()
                }
            }
            node.hasStyle(ContentStyle.WithExtraAttributes) -> div {
                node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue }
                childrenCallback()
            }
            node.dci.kind in setOf(ContentKind.Symbol) -> div("symbol $additionalClasses") {
                childrenCallback()
            }
            node.hasStyle(ContentStyle.KDocTag) -> span("kdoc-tag") { childrenCallback() }
            node.hasStyle(ContentStyle.Footnote) -> div("footnote") { childrenCallback() }
            node.hasStyle(TextStyle.BreakableAfter) -> {
                span { childrenCallback() }
                wbr { }
            }
            node.hasStyle(TextStyle.Breakable) -> {
                span("breakable-word") { childrenCallback() }
            }
            node.hasStyle(TextStyle.Span) -> span { childrenCallback() }
            node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") {
                childrenCallback()
            }
            node.dci.kind == SymbolContentKind.Parameters -> {
                span("parameters $additionalClasses") {
                    childrenCallback()
                }
            }
            node.dci.kind == SymbolContentKind.Parameter -> {
                span("parameter $additionalClasses") {
                    childrenCallback()
                }
            }
            node.hasStyle(TextStyle.InlineComment) -> div("inline-comment") { childrenCallback() }
            node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() }
            node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { //TODO this can be removed
                childrenCallback()
            }
            node.dci.kind == ContentKind.Deprecation -> div("deprecation-content") { childrenCallback() }
            node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() }
            node.hasStyle(TextStyle.Block) -> div(additionalClasses) {
                childrenCallback()
            }
            node.hasStyle(TextStyle.Quotation) -> blockQuote(additionalClasses) { childrenCallback() }
            node.hasStyle(TextStyle.FloatingRight) -> span("clearfix") { span("floating-right") { childrenCallback() } }
            node.hasStyle(TextStyle.Strikethrough) -> strike { childrenCallback() }
            node.isAnchorable -> buildAnchor(
                node.anchor!!,
                node.anchorLabel!!,
                node.buildSourceSetFilterValues()
            ) { childrenCallback() }
            node.extra[InsertTemplateExtra] != null -> node.extra[InsertTemplateExtra]?.let { templateCommand(it.command) }
                ?: Unit
            node.hasStyle(ListStyle.DescriptionTerm) -> DT(emptyMap(), consumer).visit {
                [email protected]()
            }
            node.hasStyle(ListStyle.DescriptionDetails) -> DD(emptyMap(), consumer).visit {
                [email protected]()
            }
            node.extra.extraTabbedContentType() != null -> div() {
                node.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() }
                [email protected]()
            }
            else -> childrenCallback()
        }
    }

    private fun FlowContent.copyButton() = span(classes = "top-right-position") {
        span("copy-icon")
        copiedPopup("Content copied to clipboard", "popup-to-left")
    }

    private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") =
        div("copy-popup-wrapper $additionalClasses") {
            span("copy-popup-icon")
            span {
                text(notificationContent)
            }
        }

    override fun FlowContent.buildPlatformDependent(
        content: PlatformHintedContent,
        pageContext: ContentPage,
        sourceSetRestriction: Set?
    ) {
        buildPlatformDependent(
            content.sourceSets.filter {
                sourceSetRestriction == null || it in sourceSetRestriction
            }.associateWith { setOf(content.inner) },
            pageContext,
            content.extra,
            content.style
        )
    }

    private fun FlowContent.buildPlatformDependent(
        nodes: Map>,
        pageContext: ContentPage,
        extra: PropertyContainer = PropertyContainer.empty(),
        styles: Set