/* * Copyright 2014-2023 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.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.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 } /** * 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 { this@wrapGroup.childrenCallback() } node.hasStyle(ListStyle.DescriptionDetails) -> DD(emptyMap(), consumer).visit { this@wrapGroup.childrenCallback() } node.extra.extraTabbedContentType() != null -> div() { node.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() } this@wrapGroup.childrenCallback() } 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