package org.jetbrains.dokka.base.renderers.html import kotlinx.html.* import kotlinx.html.stream.createHTML import org.jetbrains.dokka.DokkaSourceSetID 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.links.DRI import org.jetbrains.dokka.model.DisplaySourceSet import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.model.sourceSetIDs import org.jetbrains.dokka.model.withDescendants import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.pages.HtmlContent import org.jetbrains.dokka.plugability.* import org.jetbrains.dokka.utilities.htmlEscape import org.jetbrains.kotlin.utils.addIfNotNull import java.net.URI internal const val TEMPLATE_REPLACEMENT: String = "###" 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 shouldRenderSourceSetBubbles: Boolean = false override val preprocessors = context.plugin().query { htmlPreprocessors } private val tabSortingStrategy = context.plugin().querySingle { tabSortingStrategy } private fun TagConsumer.prepareForTemplates() = if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this else ImmediateResolutionTagConsumer(this, context) private fun sortTabs(strategy: TabSortingStrategy, tabs: Collection): List { val sorted = strategy.sort(tabs) if (sorted.size != tabs.size) context.logger.warn("Tab sorting strategy has changed number of tabs from ${tabs.size} to ${sorted.size}") return sorted } 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 secondLevel = node.children.filterIsInstance().flatMap { it.children } .filterIsInstance().flatMap { it.children }.filterIsInstance() val firstLevel = node.children.filterIsInstance().flatMap { it.children } .filterIsInstance() val renderable = sortTabs(tabSortingStrategy, firstLevel.union(secondLevel)) div(classes = "tabs-section") { attributes["tabs-section"] = "tabs-section" renderable.forEachIndexed { index, node -> button(classes = "section-tab") { if (index == 0) attributes["data-active"] = "" attributes["data-togglable"] = node.text text(node.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(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") { if (node.hasStyle(ContentStyle.Indented)) { // could've been done with CSS (padding-left, ::before, etc), but the indent needs to // consist of physical spaces, otherwise select and copy won't work properly repeat(4) { consumer.onTagContentEntity(Entities.nbsp) } } 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.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } node.hasStyle(TextStyle.Block) -> div(additionalClasses) { childrenCallback() } node.hasStyle(TextStyle.Quotation) -> blockQuote(additionalClasses) { childrenCallback() } node.isAnchorable -> buildAnchor( node.anchor!!, node.anchorLabel!!, node.sourceSetsFilters ) { 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() } else -> childrenCallback() } } private fun FlowContent.filterButtons(page: PageNode) { if (shouldRenderSourceSetBubbles && page is ContentPage) { div(classes = "filter-section") { id = "filter-section" page.content.withDescendants().flatMap { it.sourceSets }.distinct() .sortedBy { it.comparableKey }.forEach { button(classes = "platform-tag platform-selector") { attributes["data-active"] = "" attributes["data-filter"] = it.sourceSetIDs.merged.toString() when (it.platform.key) { "common" -> classes = classes + "common-like" "native" -> classes = classes + "native-like" "jvm" -> classes = classes + "jvm-like" "js" -> classes = classes + "js-like" } text(it.name) } } } } } 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